All Files (56.88% covered at 26.2 hits/line)
534 files in total.
26674 relevant lines.
15173 lines covered and
11501 lines missed
-
# encoding:utf-8
-
#--
-
# Copyright (C) Bob Aman
-
#
-
# Licensed under the Apache License, Version 2.0 (the "License");
-
# you may not use this file except in compliance with the License.
-
# You may obtain a copy of the License at
-
#
-
# http://www.apache.org/licenses/LICENSE-2.0
-
#
-
# Unless required by applicable law or agreed to in writing, software
-
# distributed under the License is distributed on an "AS IS" BASIS,
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
# See the License for the specific language governing permissions and
-
# limitations under the License.
-
#++
-
-
-
1
begin
-
1
require "addressable/idna/native"
-
rescue LoadError
-
# libidn or the idn gem was not available, fall back on a pure-Ruby
-
# implementation...
-
1
require "addressable/idna/pure"
-
end
-
# encoding:utf-8
-
#--
-
# Copyright (C) Bob Aman
-
#
-
# Licensed under the Apache License, Version 2.0 (the "License");
-
# you may not use this file except in compliance with the License.
-
# You may obtain a copy of the License at
-
#
-
# http://www.apache.org/licenses/LICENSE-2.0
-
#
-
# Unless required by applicable law or agreed to in writing, software
-
# distributed under the License is distributed on an "AS IS" BASIS,
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
# See the License for the specific language governing permissions and
-
# limitations under the License.
-
#++
-
-
-
1
require "idn"
-
-
module Addressable
-
module IDNA
-
def self.punycode_encode(value)
-
IDN::Punycode.encode(value.to_s)
-
end
-
-
def self.punycode_decode(value)
-
IDN::Punycode.decode(value.to_s)
-
end
-
-
def self.unicode_normalize_kc(value)
-
IDN::Stringprep.nfkc_normalize(value.to_s)
-
end
-
-
def self.to_ascii(value)
-
value.to_s.split('.', -1).map do |segment|
-
if segment.size > 0 && segment.size < 64
-
IDN::Idna.toASCII(segment)
-
elsif segment.size >= 64
-
segment
-
else
-
''
-
end
-
end.join('.')
-
end
-
-
def self.to_unicode(value)
-
value.to_s.split('.', -1).map do |segment|
-
if segment.size > 0 && segment.size < 64
-
IDN::Idna.toUnicode(segment)
-
elsif segment.size >= 64
-
segment
-
else
-
''
-
end
-
end.join('.')
-
end
-
end
-
end
-
# encoding:utf-8
-
#--
-
# Copyright (C) Bob Aman
-
#
-
# Licensed under the Apache License, Version 2.0 (the "License");
-
# you may not use this file except in compliance with the License.
-
# You may obtain a copy of the License at
-
#
-
# http://www.apache.org/licenses/LICENSE-2.0
-
#
-
# Unless required by applicable law or agreed to in writing, software
-
# distributed under the License is distributed on an "AS IS" BASIS,
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
# See the License for the specific language governing permissions and
-
# limitations under the License.
-
#++
-
-
-
1
module Addressable
-
1
module IDNA
-
# This module is loosely based on idn_actionmailer by Mick Staugaard,
-
# the unicode library by Yoshida Masato, and the punycode implementation
-
# by Kazuhiro Nishiyama. Most of the code was copied verbatim, but
-
# some reformatting was done, and some translation from C was done.
-
#
-
# Without their code to work from as a base, we'd all still be relying
-
# on the presence of libidn. Which nobody ever seems to have installed.
-
#
-
# Original sources:
-
# http://github.com/staugaard/idn_actionmailer
-
# http://www.yoshidam.net/Ruby.html#unicode
-
# http://rubyforge.org/frs/?group_id=2550
-
-
-
1
UNICODE_TABLE = File.expand_path(
-
File.join(File.dirname(__FILE__), '../../..', 'data/unicode.data')
-
)
-
-
1
ACE_PREFIX = "xn--"
-
-
1
UTF8_REGEX = /\A(?:
-
[\x09\x0A\x0D\x20-\x7E] # ASCII
-
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
-
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
-
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
-
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
-
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
-
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5
-
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
-
)*\z/mnx
-
-
1
UTF8_REGEX_MULTIBYTE = /(?:
-
[\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
-
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
-
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
-
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
-
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
-
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5
-
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
-
)/mnx
-
-
# :startdoc:
-
-
# Converts from a Unicode internationalized domain name to an ASCII
-
# domain name as described in RFC 3490.
-
1
def self.to_ascii(input)
-
input = input.to_s unless input.is_a?(String)
-
input = input.dup
-
if input.respond_to?(:force_encoding)
-
input.force_encoding(Encoding::ASCII_8BIT)
-
end
-
if input =~ UTF8_REGEX && input =~ UTF8_REGEX_MULTIBYTE
-
parts = unicode_downcase(input).split('.')
-
parts.map! do |part|
-
if part.respond_to?(:force_encoding)
-
part.force_encoding(Encoding::ASCII_8BIT)
-
end
-
if part =~ UTF8_REGEX && part =~ UTF8_REGEX_MULTIBYTE
-
ACE_PREFIX + punycode_encode(unicode_normalize_kc(part))
-
else
-
part
-
end
-
end
-
parts.join('.')
-
else
-
input
-
end
-
end
-
-
# Converts from an ASCII domain name to a Unicode internationalized
-
# domain name as described in RFC 3490.
-
1
def self.to_unicode(input)
-
input = input.to_s unless input.is_a?(String)
-
parts = input.split('.')
-
parts.map! do |part|
-
if part =~ /^#{ACE_PREFIX}(.+)/
-
begin
-
punycode_decode(part[/^#{ACE_PREFIX}(.+)/, 1])
-
rescue Addressable::IDNA::PunycodeBadInput
-
# toUnicode is explicitly defined as never-fails by the spec
-
part
-
end
-
else
-
part
-
end
-
end
-
output = parts.join('.')
-
if output.respond_to?(:force_encoding)
-
output.force_encoding(Encoding::UTF_8)
-
end
-
output
-
end
-
-
# Unicode normalization form KC.
-
1
def self.unicode_normalize_kc(input)
-
input = input.to_s unless input.is_a?(String)
-
unpacked = input.unpack("U*")
-
unpacked =
-
unicode_compose(unicode_sort_canonical(unicode_decompose(unpacked)))
-
return unpacked.pack("U*")
-
end
-
-
##
-
# Unicode aware downcase method.
-
#
-
# @api private
-
# @param [String] input
-
# The input string.
-
# @return [String] The downcased result.
-
1
def self.unicode_downcase(input)
-
input = input.to_s unless input.is_a?(String)
-
unpacked = input.unpack("U*")
-
unpacked.map! { |codepoint| lookup_unicode_lowercase(codepoint) }
-
return unpacked.pack("U*")
-
end
-
2
(class <<self; private :unicode_downcase; end)
-
-
1
def self.unicode_compose(unpacked)
-
unpacked_result = []
-
length = unpacked.length
-
-
return unpacked if length == 0
-
-
starter = unpacked[0]
-
starter_cc = lookup_unicode_combining_class(starter)
-
starter_cc = 256 if starter_cc != 0
-
for i in 1...length
-
ch = unpacked[i]
-
cc = lookup_unicode_combining_class(ch)
-
-
if (starter_cc == 0 &&
-
(composite = unicode_compose_pair(starter, ch)) != nil)
-
starter = composite
-
startercc = lookup_unicode_combining_class(composite)
-
else
-
unpacked_result << starter
-
starter = ch
-
startercc = cc
-
end
-
end
-
unpacked_result << starter
-
return unpacked_result
-
end
-
2
(class <<self; private :unicode_compose; end)
-
-
1
def self.unicode_compose_pair(ch_one, ch_two)
-
if ch_one >= HANGUL_LBASE && ch_one < HANGUL_LBASE + HANGUL_LCOUNT &&
-
ch_two >= HANGUL_VBASE && ch_two < HANGUL_VBASE + HANGUL_VCOUNT
-
# Hangul L + V
-
return HANGUL_SBASE + (
-
(ch_one - HANGUL_LBASE) * HANGUL_VCOUNT + (ch_two - HANGUL_VBASE)
-
) * HANGUL_TCOUNT
-
elsif ch_one >= HANGUL_SBASE &&
-
ch_one < HANGUL_SBASE + HANGUL_SCOUNT &&
-
(ch_one - HANGUL_SBASE) % HANGUL_TCOUNT == 0 &&
-
ch_two >= HANGUL_TBASE && ch_two < HANGUL_TBASE + HANGUL_TCOUNT
-
# Hangul LV + T
-
return ch_one + (ch_two - HANGUL_TBASE)
-
end
-
-
p = []
-
ucs4_to_utf8 = lambda do |ch|
-
if ch < 128
-
p << ch
-
elsif ch < 2048
-
p << (ch >> 6 | 192)
-
p << (ch & 63 | 128)
-
elsif ch < 0x10000
-
p << (ch >> 12 | 224)
-
p << (ch >> 6 & 63 | 128)
-
p << (ch & 63 | 128)
-
elsif ch < 0x200000
-
p << (ch >> 18 | 240)
-
p << (ch >> 12 & 63 | 128)
-
p << (ch >> 6 & 63 | 128)
-
p << (ch & 63 | 128)
-
elsif ch < 0x4000000
-
p << (ch >> 24 | 248)
-
p << (ch >> 18 & 63 | 128)
-
p << (ch >> 12 & 63 | 128)
-
p << (ch >> 6 & 63 | 128)
-
p << (ch & 63 | 128)
-
elsif ch < 0x80000000
-
p << (ch >> 30 | 252)
-
p << (ch >> 24 & 63 | 128)
-
p << (ch >> 18 & 63 | 128)
-
p << (ch >> 12 & 63 | 128)
-
p << (ch >> 6 & 63 | 128)
-
p << (ch & 63 | 128)
-
end
-
end
-
-
ucs4_to_utf8.call(ch_one)
-
ucs4_to_utf8.call(ch_two)
-
-
return lookup_unicode_composition(p)
-
end
-
2
(class <<self; private :unicode_compose_pair; end)
-
-
1
def self.unicode_sort_canonical(unpacked)
-
unpacked = unpacked.dup
-
i = 1
-
length = unpacked.length
-
-
return unpacked if length < 2
-
-
while i < length
-
last = unpacked[i-1]
-
ch = unpacked[i]
-
last_cc = lookup_unicode_combining_class(last)
-
cc = lookup_unicode_combining_class(ch)
-
if cc != 0 && last_cc != 0 && last_cc > cc
-
unpacked[i] = last
-
unpacked[i-1] = ch
-
i -= 1 if i > 1
-
else
-
i += 1
-
end
-
end
-
return unpacked
-
end
-
2
(class <<self; private :unicode_sort_canonical; end)
-
-
1
def self.unicode_decompose(unpacked)
-
unpacked_result = []
-
for cp in unpacked
-
if cp >= HANGUL_SBASE && cp < HANGUL_SBASE + HANGUL_SCOUNT
-
l, v, t = unicode_decompose_hangul(cp)
-
unpacked_result << l
-
unpacked_result << v if v
-
unpacked_result << t if t
-
else
-
dc = lookup_unicode_compatibility(cp)
-
unless dc
-
unpacked_result << cp
-
else
-
unpacked_result.concat(unicode_decompose(dc.unpack("U*")))
-
end
-
end
-
end
-
return unpacked_result
-
end
-
2
(class <<self; private :unicode_decompose; end)
-
-
1
def self.unicode_decompose_hangul(codepoint)
-
sindex = codepoint - HANGUL_SBASE;
-
if sindex < 0 || sindex >= HANGUL_SCOUNT
-
l = codepoint
-
v = t = nil
-
return l, v, t
-
end
-
l = HANGUL_LBASE + sindex / HANGUL_NCOUNT
-
v = HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
-
t = HANGUL_TBASE + sindex % HANGUL_TCOUNT
-
if t == HANGUL_TBASE
-
t = nil
-
end
-
return l, v, t
-
end
-
2
(class <<self; private :unicode_decompose_hangul; end)
-
-
1
def self.lookup_unicode_combining_class(codepoint)
-
codepoint_data = UNICODE_DATA[codepoint]
-
(codepoint_data ?
-
(codepoint_data[UNICODE_DATA_COMBINING_CLASS] || 0) :
-
0)
-
end
-
2
(class <<self; private :lookup_unicode_combining_class; end)
-
-
1
def self.lookup_unicode_compatibility(codepoint)
-
codepoint_data = UNICODE_DATA[codepoint]
-
(codepoint_data ?
-
codepoint_data[UNICODE_DATA_COMPATIBILITY] : nil)
-
end
-
2
(class <<self; private :lookup_unicode_compatibility; end)
-
-
1
def self.lookup_unicode_lowercase(codepoint)
-
codepoint_data = UNICODE_DATA[codepoint]
-
(codepoint_data ?
-
(codepoint_data[UNICODE_DATA_LOWERCASE] || codepoint) :
-
codepoint)
-
end
-
2
(class <<self; private :lookup_unicode_lowercase; end)
-
-
1
def self.lookup_unicode_composition(unpacked)
-
return COMPOSITION_TABLE[unpacked]
-
end
-
2
(class <<self; private :lookup_unicode_composition; end)
-
-
1
HANGUL_SBASE = 0xac00
-
1
HANGUL_LBASE = 0x1100
-
1
HANGUL_LCOUNT = 19
-
1
HANGUL_VBASE = 0x1161
-
1
HANGUL_VCOUNT = 21
-
1
HANGUL_TBASE = 0x11a7
-
1
HANGUL_TCOUNT = 28
-
1
HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT # 588
-
1
HANGUL_SCOUNT = HANGUL_LCOUNT * HANGUL_NCOUNT # 11172
-
-
1
UNICODE_DATA_COMBINING_CLASS = 0
-
1
UNICODE_DATA_EXCLUSION = 1
-
1
UNICODE_DATA_CANONICAL = 2
-
1
UNICODE_DATA_COMPATIBILITY = 3
-
1
UNICODE_DATA_UPPERCASE = 4
-
1
UNICODE_DATA_LOWERCASE = 5
-
1
UNICODE_DATA_TITLECASE = 6
-
-
1
begin
-
1
if defined?(FakeFS)
-
fakefs_state = FakeFS.activated?
-
FakeFS.deactivate!
-
end
-
# This is a sparse Unicode table. Codepoints without entries are
-
# assumed to have the value: [0, 0, nil, nil, nil, nil, nil]
-
1
UNICODE_DATA = File.open(UNICODE_TABLE, "rb") do |file|
-
1
Marshal.load(file.read)
-
end
-
ensure
-
1
if defined?(FakeFS)
-
FakeFS.activate! if fakefs_state
-
end
-
end
-
-
1
COMPOSITION_TABLE = {}
-
1
for codepoint, data in UNICODE_DATA
-
4233
canonical = data[UNICODE_DATA_CANONICAL]
-
4233
exclusion = data[UNICODE_DATA_EXCLUSION]
-
-
4233
if canonical && exclusion == 0
-
918
COMPOSITION_TABLE[canonical.unpack("C*")] = codepoint
-
end
-
end
-
-
1
UNICODE_MAX_LENGTH = 256
-
1
ACE_MAX_LENGTH = 256
-
-
1
PUNYCODE_BASE = 36
-
1
PUNYCODE_TMIN = 1
-
1
PUNYCODE_TMAX = 26
-
1
PUNYCODE_SKEW = 38
-
1
PUNYCODE_DAMP = 700
-
1
PUNYCODE_INITIAL_BIAS = 72
-
1
PUNYCODE_INITIAL_N = 0x80
-
1
PUNYCODE_DELIMITER = 0x2D
-
-
1
PUNYCODE_MAXINT = 1 << 64
-
-
1
PUNYCODE_PRINT_ASCII =
-
"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
-
"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
-
" !\"\#$%&'()*+,-./" +
-
"0123456789:;<=>?" +
-
"@ABCDEFGHIJKLMNO" +
-
"PQRSTUVWXYZ[\\]^_" +
-
"`abcdefghijklmno" +
-
"pqrstuvwxyz{|}~\n"
-
-
# Input is invalid.
-
1
class PunycodeBadInput < StandardError; end
-
# Output would exceed the space provided.
-
1
class PunycodeBigOutput < StandardError; end
-
# Input needs wider integers to process.
-
1
class PunycodeOverflow < StandardError; end
-
-
1
def self.punycode_encode(unicode)
-
unicode = unicode.to_s unless unicode.is_a?(String)
-
input = unicode.unpack("U*")
-
output = [0] * (ACE_MAX_LENGTH + 1)
-
input_length = input.size
-
output_length = [ACE_MAX_LENGTH]
-
-
# Initialize the state
-
n = PUNYCODE_INITIAL_N
-
delta = out = 0
-
max_out = output_length[0]
-
bias = PUNYCODE_INITIAL_BIAS
-
-
# Handle the basic code points:
-
input_length.times do |j|
-
if punycode_basic?(input[j])
-
if max_out - out < 2
-
raise PunycodeBigOutput,
-
"Output would exceed the space provided."
-
end
-
output[out] = input[j]
-
out += 1
-
end
-
end
-
-
h = b = out
-
-
# h is the number of code points that have been handled, b is the
-
# number of basic code points, and out is the number of characters
-
# that have been output.
-
-
if b > 0
-
output[out] = PUNYCODE_DELIMITER
-
out += 1
-
end
-
-
# Main encoding loop:
-
-
while h < input_length
-
# All non-basic code points < n have been
-
# handled already. Find the next larger one:
-
-
m = PUNYCODE_MAXINT
-
input_length.times do |j|
-
m = input[j] if (n...m) === input[j]
-
end
-
-
# Increase delta enough to advance the decoder's
-
# <n,i> state to <m,0>, but guard against overflow:
-
-
if m - n > (PUNYCODE_MAXINT - delta) / (h + 1)
-
raise PunycodeOverflow, "Input needs wider integers to process."
-
end
-
delta += (m - n) * (h + 1)
-
n = m
-
-
input_length.times do |j|
-
# Punycode does not need to check whether input[j] is basic:
-
if input[j] < n
-
delta += 1
-
if delta == 0
-
raise PunycodeOverflow,
-
"Input needs wider integers to process."
-
end
-
end
-
-
if input[j] == n
-
# Represent delta as a generalized variable-length integer:
-
-
q = delta; k = PUNYCODE_BASE
-
while true
-
if out >= max_out
-
raise PunycodeBigOutput,
-
"Output would exceed the space provided."
-
end
-
t = (
-
if k <= bias
-
PUNYCODE_TMIN
-
elsif k >= bias + PUNYCODE_TMAX
-
PUNYCODE_TMAX
-
else
-
k - bias
-
end
-
)
-
break if q < t
-
output[out] =
-
punycode_encode_digit(t + (q - t) % (PUNYCODE_BASE - t))
-
out += 1
-
q = (q - t) / (PUNYCODE_BASE - t)
-
k += PUNYCODE_BASE
-
end
-
-
output[out] = punycode_encode_digit(q)
-
out += 1
-
bias = punycode_adapt(delta, h + 1, h == b)
-
delta = 0
-
h += 1
-
end
-
end
-
-
delta += 1
-
n += 1
-
end
-
-
output_length[0] = out
-
-
outlen = out
-
outlen.times do |j|
-
c = output[j]
-
unless c >= 0 && c <= 127
-
raise StandardError, "Invalid output char."
-
end
-
unless PUNYCODE_PRINT_ASCII[c]
-
raise PunycodeBadInput, "Input is invalid."
-
end
-
end
-
-
output[0..outlen].map { |x| x.chr }.join("").sub(/\0+\z/, "")
-
end
-
2
(class <<self; private :punycode_encode; end)
-
-
1
def self.punycode_decode(punycode)
-
input = []
-
output = []
-
-
if ACE_MAX_LENGTH * 2 < punycode.size
-
raise PunycodeBigOutput, "Output would exceed the space provided."
-
end
-
punycode.each_byte do |c|
-
unless c >= 0 && c <= 127
-
raise PunycodeBadInput, "Input is invalid."
-
end
-
input.push(c)
-
end
-
-
input_length = input.length
-
output_length = [UNICODE_MAX_LENGTH]
-
-
# Initialize the state
-
n = PUNYCODE_INITIAL_N
-
-
out = i = 0
-
max_out = output_length[0]
-
bias = PUNYCODE_INITIAL_BIAS
-
-
# Handle the basic code points: Let b be the number of input code
-
# points before the last delimiter, or 0 if there is none, then
-
# copy the first b code points to the output.
-
-
b = 0
-
input_length.times do |j|
-
b = j if punycode_delimiter?(input[j])
-
end
-
if b > max_out
-
raise PunycodeBigOutput, "Output would exceed the space provided."
-
end
-
-
b.times do |j|
-
unless punycode_basic?(input[j])
-
raise PunycodeBadInput, "Input is invalid."
-
end
-
output[out] = input[j]
-
out+=1
-
end
-
-
# Main decoding loop: Start just after the last delimiter if any
-
# basic code points were copied; start at the beginning otherwise.
-
-
in_ = b > 0 ? b + 1 : 0
-
while in_ < input_length
-
-
# in_ is the index of the next character to be consumed, and
-
# out is the number of code points in the output array.
-
-
# Decode a generalized variable-length integer into delta,
-
# which gets added to i. The overflow checking is easier
-
# if we increase i as we go, then subtract off its starting
-
# value at the end to obtain delta.
-
-
oldi = i; w = 1; k = PUNYCODE_BASE
-
while true
-
if in_ >= input_length
-
raise PunycodeBadInput, "Input is invalid."
-
end
-
digit = punycode_decode_digit(input[in_])
-
in_+=1
-
if digit >= PUNYCODE_BASE
-
raise PunycodeBadInput, "Input is invalid."
-
end
-
if digit > (PUNYCODE_MAXINT - i) / w
-
raise PunycodeOverflow, "Input needs wider integers to process."
-
end
-
i += digit * w
-
t = (
-
if k <= bias
-
PUNYCODE_TMIN
-
elsif k >= bias + PUNYCODE_TMAX
-
PUNYCODE_TMAX
-
else
-
k - bias
-
end
-
)
-
break if digit < t
-
if w > PUNYCODE_MAXINT / (PUNYCODE_BASE - t)
-
raise PunycodeOverflow, "Input needs wider integers to process."
-
end
-
w *= PUNYCODE_BASE - t
-
k += PUNYCODE_BASE
-
end
-
-
bias = punycode_adapt(i - oldi, out + 1, oldi == 0)
-
-
# I was supposed to wrap around from out + 1 to 0,
-
# incrementing n each time, so we'll fix that now:
-
-
if i / (out + 1) > PUNYCODE_MAXINT - n
-
raise PunycodeOverflow, "Input needs wider integers to process."
-
end
-
n += i / (out + 1)
-
i %= out + 1
-
-
# Insert n at position i of the output:
-
-
# not needed for Punycode:
-
# raise PUNYCODE_INVALID_INPUT if decode_digit(n) <= base
-
if out >= max_out
-
raise PunycodeBigOutput, "Output would exceed the space provided."
-
end
-
-
#memmove(output + i + 1, output + i, (out - i) * sizeof *output)
-
output[i + 1, out - i] = output[i, out - i]
-
output[i] = n
-
i += 1
-
-
out += 1
-
end
-
-
output_length[0] = out
-
-
output.pack("U*")
-
end
-
2
(class <<self; private :punycode_decode; end)
-
-
1
def self.punycode_basic?(codepoint)
-
codepoint < 0x80
-
end
-
2
(class <<self; private :punycode_basic?; end)
-
-
1
def self.punycode_delimiter?(codepoint)
-
codepoint == PUNYCODE_DELIMITER
-
end
-
2
(class <<self; private :punycode_delimiter?; end)
-
-
1
def self.punycode_encode_digit(d)
-
d + 22 + 75 * ((d < 26) ? 1 : 0)
-
end
-
2
(class <<self; private :punycode_encode_digit; end)
-
-
# Returns the numeric value of a basic codepoint
-
# (for use in representing integers) in the range 0 to
-
# base - 1, or PUNYCODE_BASE if codepoint does not represent a value.
-
1
def self.punycode_decode_digit(codepoint)
-
if codepoint - 48 < 10
-
codepoint - 22
-
elsif codepoint - 65 < 26
-
codepoint - 65
-
elsif codepoint - 97 < 26
-
codepoint - 97
-
else
-
PUNYCODE_BASE
-
end
-
end
-
2
(class <<self; private :punycode_decode_digit; end)
-
-
# Bias adaptation method
-
1
def self.punycode_adapt(delta, numpoints, firsttime)
-
delta = firsttime ? delta / PUNYCODE_DAMP : delta >> 1
-
# delta >> 1 is a faster way of doing delta / 2
-
delta += delta / numpoints
-
difference = PUNYCODE_BASE - PUNYCODE_TMIN
-
-
k = 0
-
while delta > (difference * PUNYCODE_TMAX) / 2
-
delta /= difference
-
k += PUNYCODE_BASE
-
end
-
-
k + (difference + 1) * delta / (delta + PUNYCODE_SKEW)
-
end
-
2
(class <<self; private :punycode_adapt; end)
-
end
-
# :startdoc:
-
end
-
# encoding:utf-8
-
#--
-
# Copyright (C) Bob Aman
-
#
-
# Licensed under the Apache License, Version 2.0 (the "License");
-
# you may not use this file except in compliance with the License.
-
# You may obtain a copy of the License at
-
#
-
# http://www.apache.org/licenses/LICENSE-2.0
-
#
-
# Unless required by applicable law or agreed to in writing, software
-
# distributed under the License is distributed on an "AS IS" BASIS,
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
# See the License for the specific language governing permissions and
-
# limitations under the License.
-
#++
-
-
-
1
require "addressable/version"
-
1
require "addressable/idna"
-
1
require "public_suffix"
-
-
##
-
# Addressable is a library for processing links and URIs.
-
1
module Addressable
-
##
-
# This is an implementation of a URI parser based on
-
# <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>,
-
# <a href="http://www.ietf.org/rfc/rfc3987.txt">RFC 3987</a>.
-
1
class URI
-
##
-
# Raised if something other than a uri is supplied.
-
1
class InvalidURIError < StandardError
-
end
-
-
##
-
# Container for the character classes specified in
-
# <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
-
1
module CharacterClasses
-
1
ALPHA = "a-zA-Z"
-
1
DIGIT = "0-9"
-
1
GEN_DELIMS = "\\:\\/\\?\\#\\[\\]\\@"
-
1
SUB_DELIMS = "\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\="
-
1
RESERVED = GEN_DELIMS + SUB_DELIMS
-
1
UNRESERVED = ALPHA + DIGIT + "\\-\\.\\_\\~"
-
1
PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@"
-
1
SCHEME = ALPHA + DIGIT + "\\-\\+\\."
-
1
HOST = UNRESERVED + SUB_DELIMS + "\\[\\:\\]"
-
1
AUTHORITY = PCHAR
-
1
PATH = PCHAR + "\\/"
-
1
QUERY = PCHAR + "\\/\\?"
-
1
FRAGMENT = PCHAR + "\\/\\?"
-
end
-
-
1
SLASH = '/'
-
1
EMPTY_STR = ''
-
-
1
URIREGEX = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/
-
-
1
PORT_MAPPING = {
-
"http" => 80,
-
"https" => 443,
-
"ftp" => 21,
-
"tftp" => 69,
-
"sftp" => 22,
-
"ssh" => 22,
-
"svn+ssh" => 22,
-
"telnet" => 23,
-
"nntp" => 119,
-
"gopher" => 70,
-
"wais" => 210,
-
"ldap" => 389,
-
"prospero" => 1525
-
}
-
-
##
-
# Returns a URI object based on the parsed string.
-
#
-
# @param [String, Addressable::URI, #to_str] uri
-
# The URI string to parse.
-
# No parsing is performed if the object is already an
-
# <code>Addressable::URI</code>.
-
#
-
# @return [Addressable::URI] The parsed URI.
-
1
def self.parse(uri)
-
# If we were given nil, return nil.
-
2
return nil unless uri
-
# If a URI object is passed, just return itself.
-
2
return uri.dup if uri.kind_of?(self)
-
-
# If a URI object of the Ruby standard library variety is passed,
-
# convert it to a string, then parse the string.
-
# We do the check this way because we don't want to accidentally
-
# cause a missing constant exception to be thrown.
-
2
if uri.class.name =~ /^URI\b/
-
uri = uri.to_s
-
end
-
-
# Otherwise, convert to a String
-
begin
-
uri = uri.to_str
-
rescue TypeError, NoMethodError
-
raise TypeError, "Can't convert #{uri.class} into String."
-
2
end if not uri.is_a? String
-
-
# This Regexp supplied as an example in RFC 3986, and it works great.
-
2
scan = uri.scan(URIREGEX)
-
2
fragments = scan[0]
-
2
scheme = fragments[1]
-
2
authority = fragments[3]
-
2
path = fragments[4]
-
2
query = fragments[6]
-
2
fragment = fragments[8]
-
2
user = nil
-
2
password = nil
-
2
host = nil
-
2
port = nil
-
2
if authority != nil
-
# The Regexp above doesn't split apart the authority.
-
2
userinfo = authority[/^([^\[\]]*)@/, 1]
-
2
if userinfo != nil
-
user = userinfo.strip[/^([^:]*):?/, 1]
-
password = userinfo.strip[/:(.*)$/, 1]
-
end
-
2
host = authority.gsub(
-
/^([^\[\]]*)@/, EMPTY_STR
-
).gsub(
-
/:([^:@\[\]]*?)$/, EMPTY_STR
-
)
-
2
port = authority[/:([^:@\[\]]*?)$/, 1]
-
end
-
2
if port == EMPTY_STR
-
port = nil
-
end
-
-
2
return new(
-
:scheme => scheme,
-
:user => user,
-
:password => password,
-
:host => host,
-
:port => port,
-
:path => path,
-
:query => query,
-
:fragment => fragment
-
)
-
end
-
-
##
-
# Converts an input to a URI. The input does not have to be a valid
-
# URI — the method will use heuristics to guess what URI was intended.
-
# This is not standards-compliant, merely user-friendly.
-
#
-
# @param [String, Addressable::URI, #to_str] uri
-
# The URI string to parse.
-
# No parsing is performed if the object is already an
-
# <code>Addressable::URI</code>.
-
# @param [Hash] hints
-
# A <code>Hash</code> of hints to the heuristic parser.
-
# Defaults to <code>{:scheme => "http"}</code>.
-
#
-
# @return [Addressable::URI] The parsed URI.
-
1
def self.heuristic_parse(uri, hints={})
-
# If we were given nil, return nil.
-
return nil unless uri
-
# If a URI object is passed, just return itself.
-
return uri.dup if uri.kind_of?(self)
-
-
# If a URI object of the Ruby standard library variety is passed,
-
# convert it to a string, then parse the string.
-
# We do the check this way because we don't want to accidentally
-
# cause a missing constant exception to be thrown.
-
if uri.class.name =~ /^URI\b/
-
uri = uri.to_s
-
end
-
-
if !uri.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{uri.class} into String."
-
end
-
# Otherwise, convert to a String
-
uri = uri.to_str.dup.strip
-
hints = {
-
:scheme => "http"
-
}.merge(hints)
-
case uri
-
when /^http:\/+/
-
uri.gsub!(/^http:\/+/, "http://")
-
when /^https:\/+/
-
uri.gsub!(/^https:\/+/, "https://")
-
when /^feed:\/+http:\/+/
-
uri.gsub!(/^feed:\/+http:\/+/, "feed:http://")
-
when /^feed:\/+/
-
uri.gsub!(/^feed:\/+/, "feed://")
-
when /^file:\/+/
-
uri.gsub!(/^file:\/+/, "file:///")
-
when /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
-
uri.gsub!(/^/, hints[:scheme] + "://")
-
end
-
match = uri.match(URIREGEX)
-
fragments = match.captures
-
authority = fragments[3]
-
if authority && authority.length > 0
-
new_authority = authority.gsub(/\\/, '/').gsub(/ /, '%20')
-
# NOTE: We want offset 4, not 3!
-
offset = match.offset(4)
-
uri[offset[0]...offset[1]] = new_authority
-
end
-
parsed = self.parse(uri)
-
if parsed.scheme =~ /^[^\/?#\.]+\.[^\/?#]+$/
-
parsed = self.parse(hints[:scheme] + "://" + uri)
-
end
-
if parsed.path.include?(".")
-
new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
-
if new_host
-
parsed.defer_validation do
-
new_path = parsed.path.gsub(
-
Regexp.new("^" + Regexp.escape(new_host)), EMPTY_STR)
-
parsed.host = new_host
-
parsed.path = new_path
-
parsed.scheme = hints[:scheme] unless parsed.scheme
-
end
-
end
-
end
-
return parsed
-
end
-
-
##
-
# Converts a path to a file scheme URI. If the path supplied is
-
# relative, it will be returned as a relative URI. If the path supplied
-
# is actually a non-file URI, it will parse the URI as if it had been
-
# parsed with <code>Addressable::URI.parse</code>. Handles all of the
-
# various Microsoft-specific formats for specifying paths.
-
#
-
# @param [String, Addressable::URI, #to_str] path
-
# Typically a <code>String</code> path to a file or directory, but
-
# will return a sensible return value if an absolute URI is supplied
-
# instead.
-
#
-
# @return [Addressable::URI]
-
# The parsed file scheme URI or the original URI if some other URI
-
# scheme was provided.
-
#
-
# @example
-
# base = Addressable::URI.convert_path("/absolute/path/")
-
# uri = Addressable::URI.convert_path("relative/path")
-
# (base + uri).to_s
-
# #=> "file:///absolute/path/relative/path"
-
#
-
# Addressable::URI.convert_path(
-
# "c:\\windows\\My Documents 100%20\\foo.txt"
-
# ).to_s
-
# #=> "file:///c:/windows/My%20Documents%20100%20/foo.txt"
-
#
-
# Addressable::URI.convert_path("http://example.com/").to_s
-
# #=> "http://example.com/"
-
1
def self.convert_path(path)
-
# If we were given nil, return nil.
-
return nil unless path
-
# If a URI object is passed, just return itself.
-
return path if path.kind_of?(self)
-
if !path.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{path.class} into String."
-
end
-
# Otherwise, convert to a String
-
path = path.to_str.strip
-
-
path.gsub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
-
path = SLASH + path if path =~ /^([a-zA-Z])[\|:]/
-
uri = self.parse(path)
-
-
if uri.scheme == nil
-
# Adjust windows-style uris
-
uri.path.gsub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
-
"/#{$1.downcase}:/"
-
end
-
uri.path.gsub!(/\\/, SLASH)
-
if File.exist?(uri.path) &&
-
File.stat(uri.path).directory?
-
uri.path.gsub!(/\/$/, EMPTY_STR)
-
uri.path = uri.path + '/'
-
end
-
-
# If the path is absolute, set the scheme and host.
-
if uri.path =~ /^\//
-
uri.scheme = "file"
-
uri.host = EMPTY_STR
-
end
-
uri.normalize!
-
end
-
-
return uri
-
end
-
-
##
-
# Joins several URIs together.
-
#
-
# @param [String, Addressable::URI, #to_str] *uris
-
# The URIs to join.
-
#
-
# @return [Addressable::URI] The joined URI.
-
#
-
# @example
-
# base = "http://example.com/"
-
# uri = Addressable::URI.parse("relative/path")
-
# Addressable::URI.join(base, uri)
-
# #=> #<Addressable::URI:0xcab390 URI:http://example.com/relative/path>
-
1
def self.join(*uris)
-
uri_objects = uris.collect do |uri|
-
if !uri.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{uri.class} into String."
-
end
-
uri.kind_of?(self) ? uri : self.parse(uri.to_str)
-
end
-
result = uri_objects.shift.dup
-
for uri in uri_objects
-
result.join!(uri)
-
end
-
return result
-
end
-
-
##
-
# Percent encodes a URI component.
-
#
-
# @param [String, #to_str] component The URI component to encode.
-
#
-
# @param [String, Regexp] character_class
-
# The characters which are not percent encoded. If a <code>String</code>
-
# is passed, the <code>String</code> must be formatted as a regular
-
# expression character class. (Do not include the surrounding square
-
# brackets.) For example, <code>"b-zB-Z0-9"</code> would cause
-
# everything but the letters 'b' through 'z' and the numbers '0' through
-
# '9' to be percent encoded. If a <code>Regexp</code> is passed, the
-
# value <code>/[^b-zB-Z0-9]/</code> would have the same effect. A set of
-
# useful <code>String</code> values may be found in the
-
# <code>Addressable::URI::CharacterClasses</code> module. The default
-
# value is the reserved plus unreserved character classes specified in
-
# <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
-
#
-
# @param [Regexp] upcase_encoded
-
# A string of characters that may already be percent encoded, and whose
-
# encodings should be upcased. This allows normalization of percent
-
# encodings for characters not included in the
-
# <code>character_class</code>.
-
#
-
# @return [String] The encoded component.
-
#
-
# @example
-
# Addressable::URI.encode_component("simple/example", "b-zB-Z0-9")
-
# => "simple%2Fex%61mple"
-
# Addressable::URI.encode_component("simple/example", /[^b-zB-Z0-9]/)
-
# => "simple%2Fex%61mple"
-
# Addressable::URI.encode_component(
-
# "simple/example", Addressable::URI::CharacterClasses::UNRESERVED
-
# )
-
# => "simple%2Fexample"
-
1
def self.encode_component(component, character_class=
-
CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
-
upcase_encoded='')
-
return nil if component.nil?
-
-
begin
-
if component.kind_of?(Symbol) ||
-
component.kind_of?(Numeric) ||
-
component.kind_of?(TrueClass) ||
-
component.kind_of?(FalseClass)
-
component = component.to_s
-
else
-
component = component.to_str
-
end
-
rescue TypeError, NoMethodError
-
raise TypeError, "Can't convert #{component.class} into String."
-
end if !component.is_a? String
-
-
if ![String, Regexp].include?(character_class.class)
-
raise TypeError,
-
"Expected String or Regexp, got #{character_class.inspect}"
-
end
-
if character_class.kind_of?(String)
-
character_class = /[^#{character_class}]/
-
end
-
# We can't perform regexps on invalid UTF sequences, but
-
# here we need to, so switch to ASCII.
-
component = component.dup
-
component.force_encoding(Encoding::ASCII_8BIT)
-
# Avoiding gsub! because there are edge cases with frozen strings
-
component = component.gsub(character_class) do |sequence|
-
(sequence.unpack('C*').map { |c| "%" + ("%02x" % c).upcase }).join
-
end
-
if upcase_encoded.length > 0
-
component = component.gsub(/%(#{upcase_encoded.chars.map do |char|
-
char.unpack('C*').map { |c| '%02x' % c }.join
-
end.join('|')})/i) { |s| s.upcase }
-
end
-
return component
-
end
-
-
1
class << self
-
1
alias_method :encode_component, :encode_component
-
end
-
-
##
-
# Unencodes any percent encoded characters within a URI component.
-
# This method may be used for unencoding either components or full URIs,
-
# however, it is recommended to use the <code>unencode_component</code>
-
# alias when unencoding components.
-
#
-
# @param [String, Addressable::URI, #to_str] uri
-
# The URI or component to unencode.
-
#
-
# @param [Class] return_type
-
# The type of object to return.
-
# This value may only be set to <code>String</code> or
-
# <code>Addressable::URI</code>. All other values are invalid. Defaults
-
# to <code>String</code>.
-
#
-
# @param [String] leave_encoded
-
# A string of characters to leave encoded. If a percent encoded character
-
# in this list is encountered then it will remain percent encoded.
-
#
-
# @return [String, Addressable::URI]
-
# The unencoded component or URI.
-
# The return type is determined by the <code>return_type</code>
-
# parameter.
-
1
def self.unencode(uri, return_type=String, leave_encoded='')
-
1
return nil if uri.nil?
-
-
begin
-
uri = uri.to_str
-
rescue NoMethodError, TypeError
-
raise TypeError, "Can't convert #{uri.class} into String."
-
1
end if !uri.is_a? String
-
1
if ![String, ::Addressable::URI].include?(return_type)
-
raise TypeError,
-
"Expected Class (String or Addressable::URI), " +
-
"got #{return_type.inspect}"
-
end
-
1
uri = uri.dup
-
# Seriously, only use UTF-8. I'm really not kidding!
-
1
uri.force_encoding("utf-8")
-
1
leave_encoded.force_encoding("utf-8")
-
1
result = uri.gsub(/%[0-9a-f]{2}/iu) do |sequence|
-
c = sequence[1..3].to_i(16).chr
-
c.force_encoding("utf-8")
-
leave_encoded.include?(c) ? sequence : c
-
end
-
1
result.force_encoding("utf-8")
-
1
if return_type == String
-
1
return result
-
elsif return_type == ::Addressable::URI
-
return ::Addressable::URI.parse(result)
-
end
-
end
-
-
1
class << self
-
1
alias_method :unescape, :unencode
-
1
alias_method :unencode_component, :unencode
-
1
alias_method :unescape_component, :unencode
-
end
-
-
-
##
-
# Normalizes the encoding of a URI component.
-
#
-
# @param [String, #to_str] component The URI component to encode.
-
#
-
# @param [String, Regexp] character_class
-
# The characters which are not percent encoded. If a <code>String</code>
-
# is passed, the <code>String</code> must be formatted as a regular
-
# expression character class. (Do not include the surrounding square
-
# brackets.) For example, <code>"b-zB-Z0-9"</code> would cause
-
# everything but the letters 'b' through 'z' and the numbers '0'
-
# through '9' to be percent encoded. If a <code>Regexp</code> is passed,
-
# the value <code>/[^b-zB-Z0-9]/</code> would have the same effect. A
-
# set of useful <code>String</code> values may be found in the
-
# <code>Addressable::URI::CharacterClasses</code> module. The default
-
# value is the reserved plus unreserved character classes specified in
-
# <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
-
#
-
# @param [String] leave_encoded
-
# When <code>character_class</code> is a <code>String</code> then
-
# <code>leave_encoded</code> is a string of characters that should remain
-
# percent encoded while normalizing the component; if they appear percent
-
# encoded in the original component, then they will be upcased ("%2f"
-
# normalized to "%2F") but otherwise left alone.
-
#
-
# @return [String] The normalized component.
-
#
-
# @example
-
# Addressable::URI.normalize_component("simpl%65/%65xampl%65", "b-zB-Z")
-
# => "simple%2Fex%61mple"
-
# Addressable::URI.normalize_component(
-
# "simpl%65/%65xampl%65", /[^b-zB-Z]/
-
# )
-
# => "simple%2Fex%61mple"
-
# Addressable::URI.normalize_component(
-
# "simpl%65/%65xampl%65",
-
# Addressable::URI::CharacterClasses::UNRESERVED
-
# )
-
# => "simple%2Fexample"
-
# Addressable::URI.normalize_component(
-
# "one%20two%2fthree%26four",
-
# "0-9a-zA-Z &/",
-
# "/"
-
# )
-
# => "one two%2Fthree&four"
-
1
def self.normalize_component(component, character_class=
-
CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
-
leave_encoded='')
-
return nil if component.nil?
-
-
begin
-
component = component.to_str
-
rescue NoMethodError, TypeError
-
raise TypeError, "Can't convert #{component.class} into String."
-
end if !component.is_a? String
-
-
if ![String, Regexp].include?(character_class.class)
-
raise TypeError,
-
"Expected String or Regexp, got #{character_class.inspect}"
-
end
-
if character_class.kind_of?(String)
-
leave_re = if leave_encoded.length > 0
-
character_class = "#{character_class}%" unless character_class.include?('%')
-
-
"|%(?!#{leave_encoded.chars.map do |char|
-
seq = char.unpack('C*').map { |c| '%02x' % c }.join
-
[seq.upcase, seq.downcase]
-
end.flatten.join('|')})"
-
end
-
-
character_class = /[^#{character_class}]#{leave_re}/
-
end
-
# We can't perform regexps on invalid UTF sequences, but
-
# here we need to, so switch to ASCII.
-
component = component.dup
-
component.force_encoding(Encoding::ASCII_8BIT)
-
unencoded = self.unencode_component(component, String, leave_encoded)
-
begin
-
encoded = self.encode_component(
-
Addressable::IDNA.unicode_normalize_kc(unencoded),
-
character_class,
-
leave_encoded
-
)
-
rescue ArgumentError
-
encoded = self.encode_component(unencoded)
-
end
-
encoded.force_encoding(Encoding::UTF_8)
-
return encoded
-
end
-
-
##
-
# Percent encodes any special characters in the URI.
-
#
-
# @param [String, Addressable::URI, #to_str] uri
-
# The URI to encode.
-
#
-
# @param [Class] return_type
-
# The type of object to return.
-
# This value may only be set to <code>String</code> or
-
# <code>Addressable::URI</code>. All other values are invalid. Defaults
-
# to <code>String</code>.
-
#
-
# @return [String, Addressable::URI]
-
# The encoded URI.
-
# The return type is determined by the <code>return_type</code>
-
# parameter.
-
1
def self.encode(uri, return_type=String)
-
return nil if uri.nil?
-
-
begin
-
uri = uri.to_str
-
rescue NoMethodError, TypeError
-
raise TypeError, "Can't convert #{uri.class} into String."
-
end if !uri.is_a? String
-
-
if ![String, ::Addressable::URI].include?(return_type)
-
raise TypeError,
-
"Expected Class (String or Addressable::URI), " +
-
"got #{return_type.inspect}"
-
end
-
uri_object = uri.kind_of?(self) ? uri : self.parse(uri)
-
encoded_uri = Addressable::URI.new(
-
:scheme => self.encode_component(uri_object.scheme,
-
Addressable::URI::CharacterClasses::SCHEME),
-
:authority => self.encode_component(uri_object.authority,
-
Addressable::URI::CharacterClasses::AUTHORITY),
-
:path => self.encode_component(uri_object.path,
-
Addressable::URI::CharacterClasses::PATH),
-
:query => self.encode_component(uri_object.query,
-
Addressable::URI::CharacterClasses::QUERY),
-
:fragment => self.encode_component(uri_object.fragment,
-
Addressable::URI::CharacterClasses::FRAGMENT)
-
)
-
if return_type == String
-
return encoded_uri.to_s
-
elsif return_type == ::Addressable::URI
-
return encoded_uri
-
end
-
end
-
-
1
class << self
-
1
alias_method :escape, :encode
-
end
-
-
##
-
# Normalizes the encoding of a URI. Characters within a hostname are
-
# not percent encoded to allow for internationalized domain names.
-
#
-
# @param [String, Addressable::URI, #to_str] uri
-
# The URI to encode.
-
#
-
# @param [Class] return_type
-
# The type of object to return.
-
# This value may only be set to <code>String</code> or
-
# <code>Addressable::URI</code>. All other values are invalid. Defaults
-
# to <code>String</code>.
-
#
-
# @return [String, Addressable::URI]
-
# The encoded URI.
-
# The return type is determined by the <code>return_type</code>
-
# parameter.
-
1
def self.normalized_encode(uri, return_type=String)
-
begin
-
uri = uri.to_str
-
rescue NoMethodError, TypeError
-
raise TypeError, "Can't convert #{uri.class} into String."
-
end if !uri.is_a? String
-
-
if ![String, ::Addressable::URI].include?(return_type)
-
raise TypeError,
-
"Expected Class (String or Addressable::URI), " +
-
"got #{return_type.inspect}"
-
end
-
uri_object = uri.kind_of?(self) ? uri : self.parse(uri)
-
components = {
-
:scheme => self.unencode_component(uri_object.scheme),
-
:user => self.unencode_component(uri_object.user),
-
:password => self.unencode_component(uri_object.password),
-
:host => self.unencode_component(uri_object.host),
-
:port => (uri_object.port.nil? ? nil : uri_object.port.to_s),
-
:path => self.unencode_component(uri_object.path),
-
:query => self.unencode_component(uri_object.query),
-
:fragment => self.unencode_component(uri_object.fragment)
-
}
-
components.each do |key, value|
-
if value != nil
-
begin
-
components[key] =
-
Addressable::IDNA.unicode_normalize_kc(value.to_str)
-
rescue ArgumentError
-
# Likely a malformed UTF-8 character, skip unicode normalization
-
components[key] = value.to_str
-
end
-
end
-
end
-
encoded_uri = Addressable::URI.new(
-
:scheme => self.encode_component(components[:scheme],
-
Addressable::URI::CharacterClasses::SCHEME),
-
:user => self.encode_component(components[:user],
-
Addressable::URI::CharacterClasses::UNRESERVED),
-
:password => self.encode_component(components[:password],
-
Addressable::URI::CharacterClasses::UNRESERVED),
-
:host => components[:host],
-
:port => components[:port],
-
:path => self.encode_component(components[:path],
-
Addressable::URI::CharacterClasses::PATH),
-
:query => self.encode_component(components[:query],
-
Addressable::URI::CharacterClasses::QUERY),
-
:fragment => self.encode_component(components[:fragment],
-
Addressable::URI::CharacterClasses::FRAGMENT)
-
)
-
if return_type == String
-
return encoded_uri.to_s
-
elsif return_type == ::Addressable::URI
-
return encoded_uri
-
end
-
end
-
-
##
-
# Encodes a set of key/value pairs according to the rules for the
-
# <code>application/x-www-form-urlencoded</code> MIME type.
-
#
-
# @param [#to_hash, #to_ary] form_values
-
# The form values to encode.
-
#
-
# @param [TrueClass, FalseClass] sort
-
# Sort the key/value pairs prior to encoding.
-
# Defaults to <code>false</code>.
-
#
-
# @return [String]
-
# The encoded value.
-
1
def self.form_encode(form_values, sort=false)
-
if form_values.respond_to?(:to_hash)
-
form_values = form_values.to_hash.to_a
-
elsif form_values.respond_to?(:to_ary)
-
form_values = form_values.to_ary
-
else
-
raise TypeError, "Can't convert #{form_values.class} into Array."
-
end
-
-
form_values = form_values.inject([]) do |accu, (key, value)|
-
if value.kind_of?(Array)
-
value.each do |v|
-
accu << [key.to_s, v.to_s]
-
end
-
else
-
accu << [key.to_s, value.to_s]
-
end
-
accu
-
end
-
-
if sort
-
# Useful for OAuth and optimizing caching systems
-
form_values = form_values.sort
-
end
-
escaped_form_values = form_values.map do |(key, value)|
-
# Line breaks are CRLF pairs
-
[
-
self.encode_component(
-
key.gsub(/(\r\n|\n|\r)/, "\r\n"),
-
CharacterClasses::UNRESERVED
-
).gsub("%20", "+"),
-
self.encode_component(
-
value.gsub(/(\r\n|\n|\r)/, "\r\n"),
-
CharacterClasses::UNRESERVED
-
).gsub("%20", "+")
-
]
-
end
-
return escaped_form_values.map do |(key, value)|
-
"#{key}=#{value}"
-
end.join("&")
-
end
-
-
##
-
# Decodes a <code>String</code> according to the rules for the
-
# <code>application/x-www-form-urlencoded</code> MIME type.
-
#
-
# @param [String, #to_str] encoded_value
-
# The form values to decode.
-
#
-
# @return [Array]
-
# The decoded values.
-
# This is not a <code>Hash</code> because of the possibility for
-
# duplicate keys.
-
1
def self.form_unencode(encoded_value)
-
if !encoded_value.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{encoded_value.class} into String."
-
end
-
encoded_value = encoded_value.to_str
-
split_values = encoded_value.split("&").map do |pair|
-
pair.split("=", 2)
-
end
-
return split_values.map do |(key, value)|
-
[
-
key ? self.unencode_component(
-
key.gsub("+", "%20")).gsub(/(\r\n|\n|\r)/, "\n") : nil,
-
value ? (self.unencode_component(
-
value.gsub("+", "%20")).gsub(/(\r\n|\n|\r)/, "\n")) : nil
-
]
-
end
-
end
-
-
##
-
# Creates a new uri object from component parts.
-
#
-
# @option [String, #to_str] scheme The scheme component.
-
# @option [String, #to_str] user The user component.
-
# @option [String, #to_str] password The password component.
-
# @option [String, #to_str] userinfo
-
# The userinfo component. If this is supplied, the user and password
-
# components must be omitted.
-
# @option [String, #to_str] host The host component.
-
# @option [String, #to_str] port The port component.
-
# @option [String, #to_str] authority
-
# The authority component. If this is supplied, the user, password,
-
# userinfo, host, and port components must be omitted.
-
# @option [String, #to_str] path The path component.
-
# @option [String, #to_str] query The query component.
-
# @option [String, #to_str] fragment The fragment component.
-
#
-
# @return [Addressable::URI] The constructed URI object.
-
1
def initialize(options={})
-
2
if options.has_key?(:authority)
-
if (options.keys & [:userinfo, :user, :password, :host, :port]).any?
-
raise ArgumentError,
-
"Cannot specify both an authority and any of the components " +
-
"within the authority."
-
end
-
end
-
2
if options.has_key?(:userinfo)
-
if (options.keys & [:user, :password]).any?
-
raise ArgumentError,
-
"Cannot specify both a userinfo and either the user or password."
-
end
-
end
-
-
2
self.defer_validation do
-
# Bunch of crazy logic required because of the composite components
-
# like userinfo and authority.
-
2
self.scheme = options[:scheme] if options[:scheme]
-
2
self.user = options[:user] if options[:user]
-
2
self.password = options[:password] if options[:password]
-
2
self.userinfo = options[:userinfo] if options[:userinfo]
-
2
self.host = options[:host] if options[:host]
-
2
self.port = options[:port] if options[:port]
-
2
self.authority = options[:authority] if options[:authority]
-
2
self.path = options[:path] if options[:path]
-
2
self.query = options[:query] if options[:query]
-
2
self.query_values = options[:query_values] if options[:query_values]
-
2
self.fragment = options[:fragment] if options[:fragment]
-
end
-
2
self.to_s
-
end
-
-
##
-
# Freeze URI, initializing instance variables.
-
#
-
# @return [Addressable::URI] The frozen URI object.
-
1
def freeze
-
self.normalized_scheme
-
self.normalized_user
-
self.normalized_password
-
self.normalized_userinfo
-
self.normalized_host
-
self.normalized_port
-
self.normalized_authority
-
self.normalized_site
-
self.normalized_path
-
self.normalized_query
-
self.normalized_fragment
-
self.hash
-
super
-
end
-
-
##
-
# The scheme component for this URI.
-
#
-
# @return [String] The scheme component.
-
1
def scheme
-
14
return defined?(@scheme) ? @scheme : nil
-
end
-
-
##
-
# The scheme component for this URI, normalized.
-
#
-
# @return [String] The scheme component, normalized.
-
1
def normalized_scheme
-
return nil unless self.scheme
-
@normalized_scheme ||= begin
-
if self.scheme =~ /^\s*ssh\+svn\s*$/i
-
"svn+ssh"
-
else
-
Addressable::URI.normalize_component(
-
self.scheme.strip.downcase,
-
Addressable::URI::CharacterClasses::SCHEME
-
)
-
end
-
end
-
# All normalized values should be UTF-8
-
@normalized_scheme.force_encoding(Encoding::UTF_8) if @normalized_scheme
-
@normalized_scheme
-
end
-
-
##
-
# Sets the scheme component for this URI.
-
#
-
# @param [String, #to_str] new_scheme The new scheme component.
-
1
def scheme=(new_scheme)
-
2
if new_scheme && !new_scheme.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_scheme.class} into String."
-
2
elsif new_scheme
-
2
new_scheme = new_scheme.to_str
-
end
-
2
if new_scheme && new_scheme !~ /\A[a-z][a-z0-9\.\+\-]*\z/i
-
raise InvalidURIError, "Invalid scheme format: #{new_scheme}"
-
end
-
2
@scheme = new_scheme
-
2
@scheme = nil if @scheme.to_s.strip.empty?
-
-
# Reset dependent values
-
2
remove_instance_variable(:@normalized_scheme) if defined?(@normalized_scheme)
-
2
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
2
validate()
-
end
-
-
##
-
# The user component for this URI.
-
#
-
# @return [String] The user component.
-
1
def user
-
3
return defined?(@user) ? @user : nil
-
end
-
-
##
-
# The user component for this URI, normalized.
-
#
-
# @return [String] The user component, normalized.
-
1
def normalized_user
-
return nil unless self.user
-
return @normalized_user if defined?(@normalized_user)
-
@normalized_user ||= begin
-
if normalized_scheme =~ /https?/ && self.user.strip.empty? &&
-
(!self.password || self.password.strip.empty?)
-
nil
-
else
-
Addressable::URI.normalize_component(
-
self.user.strip,
-
Addressable::URI::CharacterClasses::UNRESERVED
-
)
-
end
-
end
-
# All normalized values should be UTF-8
-
@normalized_user.force_encoding(Encoding::UTF_8) if @normalized_user
-
@normalized_user
-
end
-
-
##
-
# Sets the user component for this URI.
-
#
-
# @param [String, #to_str] new_user The new user component.
-
1
def user=(new_user)
-
if new_user && !new_user.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_user.class} into String."
-
end
-
@user = new_user ? new_user.to_str : nil
-
-
# You can't have a nil user with a non-nil password
-
if password != nil
-
@user = EMPTY_STR if @user.nil?
-
end
-
-
# Reset dependent values
-
remove_instance_variable(:@userinfo) if defined?(@userinfo)
-
remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
-
remove_instance_variable(:@authority) if defined?(@authority)
-
remove_instance_variable(:@normalized_user) if defined?(@normalized_user)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
##
-
# The password component for this URI.
-
#
-
# @return [String] The password component.
-
1
def password
-
3
return defined?(@password) ? @password : nil
-
end
-
-
##
-
# The password component for this URI, normalized.
-
#
-
# @return [String] The password component, normalized.
-
1
def normalized_password
-
return nil unless self.password
-
return @normalized_password if defined?(@normalized_password)
-
@normalized_password ||= begin
-
if self.normalized_scheme =~ /https?/ && self.password.strip.empty? &&
-
(!self.user || self.user.strip.empty?)
-
nil
-
else
-
Addressable::URI.normalize_component(
-
self.password.strip,
-
Addressable::URI::CharacterClasses::UNRESERVED
-
)
-
end
-
end
-
# All normalized values should be UTF-8
-
if @normalized_password
-
@normalized_password.force_encoding(Encoding::UTF_8)
-
end
-
@normalized_password
-
end
-
-
##
-
# Sets the password component for this URI.
-
#
-
# @param [String, #to_str] new_password The new password component.
-
1
def password=(new_password)
-
if new_password && !new_password.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_password.class} into String."
-
end
-
@password = new_password ? new_password.to_str : nil
-
-
# You can't have a nil user with a non-nil password
-
@password ||= nil
-
@user ||= nil
-
if @password != nil
-
@user = EMPTY_STR if @user.nil?
-
end
-
-
# Reset dependent values
-
remove_instance_variable(:@userinfo) if defined?(@userinfo)
-
remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
-
remove_instance_variable(:@authority) if defined?(@authority)
-
remove_instance_variable(:@normalized_password) if defined?(@normalized_password)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
##
-
# The userinfo component for this URI.
-
# Combines the user and password components.
-
#
-
# @return [String] The userinfo component.
-
1
def userinfo
-
2
current_user = self.user
-
2
current_password = self.password
-
2
(current_user || current_password) && @userinfo ||= begin
-
if current_user && current_password
-
"#{current_user}:#{current_password}"
-
elsif current_user && !current_password
-
"#{current_user}"
-
end
-
2
end
-
end
-
-
##
-
# The userinfo component for this URI, normalized.
-
#
-
# @return [String] The userinfo component, normalized.
-
1
def normalized_userinfo
-
return nil unless self.userinfo
-
return @normalized_userinfo if defined?(@normalized_userinfo)
-
@normalized_userinfo ||= begin
-
current_user = self.normalized_user
-
current_password = self.normalized_password
-
if !current_user && !current_password
-
nil
-
elsif current_user && current_password
-
"#{current_user}:#{current_password}"
-
elsif current_user && !current_password
-
"#{current_user}"
-
end
-
end
-
# All normalized values should be UTF-8
-
if @normalized_userinfo
-
@normalized_userinfo.force_encoding(Encoding::UTF_8)
-
end
-
@normalized_userinfo
-
end
-
-
##
-
# Sets the userinfo component for this URI.
-
#
-
# @param [String, #to_str] new_userinfo The new userinfo component.
-
1
def userinfo=(new_userinfo)
-
if new_userinfo && !new_userinfo.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_userinfo.class} into String."
-
end
-
new_user, new_password = if new_userinfo
-
[
-
new_userinfo.to_str.strip[/^(.*):/, 1],
-
new_userinfo.to_str.strip[/:(.*)$/, 1]
-
]
-
else
-
[nil, nil]
-
end
-
-
# Password assigned first to ensure validity in case of nil
-
self.password = new_password
-
self.user = new_user
-
-
# Reset dependent values
-
remove_instance_variable(:@authority) if defined?(@authority)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
##
-
# The host component for this URI.
-
#
-
# @return [String] The host component.
-
1
def host
-
17
return defined?(@host) ? @host : nil
-
end
-
-
##
-
# The host component for this URI, normalized.
-
#
-
# @return [String] The host component, normalized.
-
1
def normalized_host
-
return nil unless self.host
-
@normalized_host ||= begin
-
if !self.host.strip.empty?
-
result = ::Addressable::IDNA.to_ascii(
-
URI.unencode_component(self.host.strip.downcase)
-
)
-
if result =~ /[^\.]\.$/
-
# Single trailing dots are unnecessary.
-
result = result[0...-1]
-
end
-
result = Addressable::URI.normalize_component(
-
result,
-
CharacterClasses::HOST)
-
result
-
else
-
EMPTY_STR
-
end
-
end
-
# All normalized values should be UTF-8
-
@normalized_host.force_encoding(Encoding::UTF_8) if @normalized_host
-
@normalized_host
-
end
-
-
##
-
# Sets the host component for this URI.
-
#
-
# @param [String, #to_str] new_host The new host component.
-
1
def host=(new_host)
-
2
if new_host && !new_host.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_host.class} into String."
-
end
-
2
@host = new_host ? new_host.to_str : nil
-
-
# Reset dependent values
-
2
remove_instance_variable(:@authority) if defined?(@authority)
-
2
remove_instance_variable(:@normalized_host) if defined?(@normalized_host)
-
2
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
2
validate()
-
end
-
-
##
-
# This method is same as URI::Generic#host except
-
# brackets for IPv6 (and 'IPvFuture') addresses are removed.
-
#
-
# @see Addressable::URI#host
-
#
-
# @return [String] The hostname for this URI.
-
1
def hostname
-
v = self.host
-
/\A\[(.*)\]\z/ =~ v ? $1 : v
-
end
-
-
##
-
# This method is same as URI::Generic#host= except
-
# the argument can be a bare IPv6 address (or 'IPvFuture').
-
#
-
# @see Addressable::URI#host=
-
#
-
# @param [String, #to_str] new_hostname The new hostname for this URI.
-
1
def hostname=(new_hostname)
-
if new_hostname &&
-
(new_hostname.respond_to?(:ipv4?) || new_hostname.respond_to?(:ipv6?))
-
new_hostname = new_hostname.to_s
-
elsif new_hostname && !new_hostname.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_hostname.class} into String."
-
end
-
v = new_hostname ? new_hostname.to_str : nil
-
v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v
-
self.host = v
-
end
-
-
##
-
# Returns the top-level domain for this host.
-
#
-
# @example
-
# Addressable::URI.parse("www.example.co.uk").tld # => "co.uk"
-
1
def tld
-
PublicSuffix.parse(self.host, ignore_private: true).tld
-
end
-
-
##
-
# Returns the public suffix domain for this host.
-
#
-
# @example
-
# Addressable::URI.parse("www.example.co.uk").domain # => "example.co.uk"
-
1
def domain
-
PublicSuffix.domain(self.host, ignore_private: true)
-
end
-
-
##
-
# The authority component for this URI.
-
# Combines the user, password, host, and port components.
-
#
-
# @return [String] The authority component.
-
1
def authority
-
self.host && @authority ||= begin
-
2
authority = String.new
-
2
if self.userinfo != nil
-
authority << "#{self.userinfo}@"
-
end
-
2
authority << self.host
-
2
if self.port != nil
-
1
authority << ":#{self.port}"
-
end
-
2
authority
-
4
end
-
end
-
-
##
-
# The authority component for this URI, normalized.
-
#
-
# @return [String] The authority component, normalized.
-
1
def normalized_authority
-
return nil unless self.authority
-
@normalized_authority ||= begin
-
authority = String.new
-
if self.normalized_userinfo != nil
-
authority << "#{self.normalized_userinfo}@"
-
end
-
authority << self.normalized_host
-
if self.normalized_port != nil
-
authority << ":#{self.normalized_port}"
-
end
-
authority
-
end
-
# All normalized values should be UTF-8
-
if @normalized_authority
-
@normalized_authority.force_encoding(Encoding::UTF_8)
-
end
-
@normalized_authority
-
end
-
-
##
-
# Sets the authority component for this URI.
-
#
-
# @param [String, #to_str] new_authority The new authority component.
-
1
def authority=(new_authority)
-
if new_authority
-
if !new_authority.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_authority.class} into String."
-
end
-
new_authority = new_authority.to_str
-
new_userinfo = new_authority[/^([^\[\]]*)@/, 1]
-
if new_userinfo
-
new_user = new_userinfo.strip[/^([^:]*):?/, 1]
-
new_password = new_userinfo.strip[/:(.*)$/, 1]
-
end
-
new_host = new_authority.gsub(
-
/^([^\[\]]*)@/, EMPTY_STR
-
).gsub(
-
/:([^:@\[\]]*?)$/, EMPTY_STR
-
)
-
new_port =
-
new_authority[/:([^:@\[\]]*?)$/, 1]
-
end
-
-
# Password assigned first to ensure validity in case of nil
-
self.password = defined?(new_password) ? new_password : nil
-
self.user = defined?(new_user) ? new_user : nil
-
self.host = defined?(new_host) ? new_host : nil
-
self.port = defined?(new_port) ? new_port : nil
-
-
# Reset dependent values
-
remove_instance_variable(:@userinfo) if defined?(@userinfo)
-
remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
##
-
# The origin for this URI, serialized to ASCII, as per
-
# RFC 6454, section 6.2.
-
#
-
# @return [String] The serialized origin.
-
1
def origin
-
if self.scheme && self.authority
-
if self.normalized_port
-
"#{self.normalized_scheme}://#{self.normalized_host}" +
-
":#{self.normalized_port}"
-
else
-
"#{self.normalized_scheme}://#{self.normalized_host}"
-
end
-
else
-
"null"
-
end
-
end
-
-
##
-
# Sets the origin for this URI, serialized to ASCII, as per
-
# RFC 6454, section 6.2. This assignment will reset the `userinfo`
-
# component.
-
#
-
# @param [String, #to_str] new_origin The new origin component.
-
1
def origin=(new_origin)
-
if new_origin
-
if !new_origin.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_origin.class} into String."
-
end
-
new_origin = new_origin.to_str
-
new_scheme = new_origin[/^([^:\/?#]+):\/\//, 1]
-
unless new_scheme
-
raise InvalidURIError, 'An origin cannot omit the scheme.'
-
end
-
new_host = new_origin[/:\/\/([^\/?#:]+)/, 1]
-
unless new_host
-
raise InvalidURIError, 'An origin cannot omit the host.'
-
end
-
new_port = new_origin[/:([^:@\[\]\/]*?)$/, 1]
-
end
-
-
self.scheme = defined?(new_scheme) ? new_scheme : nil
-
self.host = defined?(new_host) ? new_host : nil
-
self.port = defined?(new_port) ? new_port : nil
-
self.userinfo = nil
-
-
# Reset dependent values
-
remove_instance_variable(:@userinfo) if defined?(@userinfo)
-
remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
-
remove_instance_variable(:@authority) if defined?(@authority)
-
remove_instance_variable(:@normalized_authority) if defined?(@normalized_authority)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
# Returns an array of known ip-based schemes. These schemes typically
-
# use a similar URI form:
-
# <code>//<user>:<password>@<host>:<port>/<url-path></code>
-
1
def self.ip_based_schemes
-
2
return self.port_mapping.keys
-
end
-
-
# Returns a hash of common IP-based schemes and their default port
-
# numbers. Adding new schemes to this hash, as necessary, will allow
-
# for better URI normalization.
-
1
def self.port_mapping
-
2
PORT_MAPPING
-
end
-
-
##
-
# The port component for this URI.
-
# This is the port number actually given in the URI. This does not
-
# infer port numbers from default values.
-
#
-
# @return [Integer] The port component.
-
1
def port
-
4
return defined?(@port) ? @port : nil
-
end
-
-
##
-
# The port component for this URI, normalized.
-
#
-
# @return [Integer] The port component, normalized.
-
1
def normalized_port
-
return nil unless self.port
-
return @normalized_port if defined?(@normalized_port)
-
@normalized_port ||= begin
-
if URI.port_mapping[self.normalized_scheme] == self.port
-
nil
-
else
-
self.port
-
end
-
end
-
end
-
-
##
-
# Sets the port component for this URI.
-
#
-
# @param [String, Integer, #to_s] new_port The new port component.
-
1
def port=(new_port)
-
1
if new_port != nil && new_port.respond_to?(:to_str)
-
1
new_port = Addressable::URI.unencode_component(new_port.to_str)
-
end
-
-
1
if new_port.respond_to?(:valid_encoding?) && !new_port.valid_encoding?
-
raise InvalidURIError, "Invalid encoding in port"
-
end
-
-
1
if new_port != nil && !(new_port.to_s =~ /^\d+$/)
-
raise InvalidURIError,
-
"Invalid port number: #{new_port.inspect}"
-
end
-
-
1
@port = new_port.to_s.to_i
-
1
@port = nil if @port == 0
-
-
# Reset dependent values
-
1
remove_instance_variable(:@authority) if defined?(@authority)
-
1
remove_instance_variable(:@normalized_port) if defined?(@normalized_port)
-
1
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
1
validate()
-
end
-
-
##
-
# The inferred port component for this URI.
-
# This method will normalize to the default port for the URI's scheme if
-
# the port isn't explicitly specified in the URI.
-
#
-
# @return [Integer] The inferred port component.
-
1
def inferred_port
-
if self.port.to_i == 0
-
self.default_port
-
else
-
self.port.to_i
-
end
-
end
-
-
##
-
# The default port for this URI's scheme.
-
# This method will always returns the default port for the URI's scheme
-
# regardless of the presence of an explicit port in the URI.
-
#
-
# @return [Integer] The default port.
-
1
def default_port
-
URI.port_mapping[self.scheme.strip.downcase] if self.scheme
-
end
-
-
##
-
# The combination of components that represent a site.
-
# Combines the scheme, user, password, host, and port components.
-
# Primarily useful for HTTP and HTTPS.
-
#
-
# For example, <code>"http://example.com/path?query"</code> would have a
-
# <code>site</code> value of <code>"http://example.com"</code>.
-
#
-
# @return [String] The components that identify a site.
-
1
def site
-
(self.scheme || self.authority) && @site ||= begin
-
site_string = ""
-
site_string << "#{self.scheme}:" if self.scheme != nil
-
site_string << "//#{self.authority}" if self.authority != nil
-
site_string
-
end
-
end
-
-
##
-
# The normalized combination of components that represent a site.
-
# Combines the scheme, user, password, host, and port components.
-
# Primarily useful for HTTP and HTTPS.
-
#
-
# For example, <code>"http://example.com/path?query"</code> would have a
-
# <code>site</code> value of <code>"http://example.com"</code>.
-
#
-
# @return [String] The normalized components that identify a site.
-
1
def normalized_site
-
return nil unless self.site
-
@normalized_site ||= begin
-
site_string = ""
-
if self.normalized_scheme != nil
-
site_string << "#{self.normalized_scheme}:"
-
end
-
if self.normalized_authority != nil
-
site_string << "//#{self.normalized_authority}"
-
end
-
site_string
-
end
-
# All normalized values should be UTF-8
-
@normalized_site.force_encoding(Encoding::UTF_8) if @normalized_site
-
@normalized_site
-
end
-
-
##
-
# Sets the site value for this URI.
-
#
-
# @param [String, #to_str] new_site The new site value.
-
1
def site=(new_site)
-
if new_site
-
if !new_site.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_site.class} into String."
-
end
-
new_site = new_site.to_str
-
# These two regular expressions derived from the primary parsing
-
# expression
-
self.scheme = new_site[/^(?:([^:\/?#]+):)?(?:\/\/(?:[^\/?#]*))?$/, 1]
-
self.authority = new_site[
-
/^(?:(?:[^:\/?#]+):)?(?:\/\/([^\/?#]*))?$/, 1
-
]
-
else
-
self.scheme = nil
-
self.authority = nil
-
end
-
end
-
-
##
-
# The path component for this URI.
-
#
-
# @return [String] The path component.
-
1
def path
-
16
return defined?(@path) ? @path : EMPTY_STR
-
end
-
-
1
NORMPATH = /^(?!\/)[^\/:]*:.*$/
-
##
-
# The path component for this URI, normalized.
-
#
-
# @return [String] The path component, normalized.
-
1
def normalized_path
-
@normalized_path ||= begin
-
path = self.path.to_s
-
if self.scheme == nil && path =~ NORMPATH
-
# Relative paths with colons in the first segment are ambiguous.
-
path = path.sub(":", "%2F")
-
end
-
# String#split(delimeter, -1) uses the more strict splitting behavior
-
# found by default in Python.
-
result = path.strip.split(SLASH, -1).map do |segment|
-
Addressable::URI.normalize_component(
-
segment,
-
Addressable::URI::CharacterClasses::PCHAR
-
)
-
end.join(SLASH)
-
-
result = URI.normalize_path(result)
-
if result.empty? &&
-
["http", "https", "ftp", "tftp"].include?(self.normalized_scheme)
-
result = SLASH
-
end
-
result
-
end
-
# All normalized values should be UTF-8
-
@normalized_path.force_encoding(Encoding::UTF_8) if @normalized_path
-
@normalized_path
-
end
-
-
##
-
# Sets the path component for this URI.
-
#
-
# @param [String, #to_str] new_path The new path component.
-
1
def path=(new_path)
-
2
if new_path && !new_path.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_path.class} into String."
-
end
-
2
@path = (new_path || EMPTY_STR).to_str
-
2
if !@path.empty? && @path[0..0] != SLASH && host != nil
-
@path = "/#{@path}"
-
end
-
-
# Reset dependent values
-
2
remove_instance_variable(:@normalized_path) if defined?(@normalized_path)
-
2
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
2
validate()
-
end
-
-
##
-
# The basename, if any, of the file in the path component.
-
#
-
# @return [String] The path's basename.
-
1
def basename
-
# Path cannot be nil
-
return File.basename(self.path).gsub(/;[^\/]*$/, EMPTY_STR)
-
end
-
-
##
-
# The extname, if any, of the file in the path component.
-
# Empty string if there is no extension.
-
#
-
# @return [String] The path's extname.
-
1
def extname
-
return nil unless self.path
-
return File.extname(self.basename)
-
end
-
-
##
-
# The query component for this URI.
-
#
-
# @return [String] The query component.
-
1
def query
-
3
return defined?(@query) ? @query : nil
-
end
-
-
##
-
# The query component for this URI, normalized.
-
#
-
# @return [String] The query component, normalized.
-
1
def normalized_query(*flags)
-
return nil unless self.query
-
return @normalized_query if defined?(@normalized_query)
-
@normalized_query ||= begin
-
modified_query_class = Addressable::URI::CharacterClasses::QUERY.dup
-
# Make sure possible key-value pair delimiters are escaped.
-
modified_query_class.sub!("\\&", "").sub!("\\;", "")
-
pairs = (self.query || "").split("&", -1)
-
pairs.sort! if flags.include?(:sorted)
-
component = pairs.map do |pair|
-
Addressable::URI.normalize_component(pair, modified_query_class, "+")
-
end.join("&")
-
component == "" ? nil : component
-
end
-
# All normalized values should be UTF-8
-
@normalized_query.force_encoding(Encoding::UTF_8) if @normalized_query
-
@normalized_query
-
end
-
-
##
-
# Sets the query component for this URI.
-
#
-
# @param [String, #to_str] new_query The new query component.
-
1
def query=(new_query)
-
if new_query && !new_query.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_query.class} into String."
-
end
-
@query = new_query ? new_query.to_str : nil
-
-
# Reset dependent values
-
remove_instance_variable(:@normalized_query) if defined?(@normalized_query)
-
remove_composite_values
-
end
-
-
##
-
# Converts the query component to a Hash value.
-
#
-
# @param [Class] return_type The return type desired. Value must be either
-
# `Hash` or `Array`.
-
#
-
# @return [Hash, Array, nil] The query string parsed as a Hash or Array
-
# or nil if the query string is blank.
-
#
-
# @example
-
# Addressable::URI.parse("?one=1&two=2&three=3").query_values
-
# #=> {"one" => "1", "two" => "2", "three" => "3"}
-
# Addressable::URI.parse("?one=two&one=three").query_values(Array)
-
# #=> [["one", "two"], ["one", "three"]]
-
# Addressable::URI.parse("?one=two&one=three").query_values(Hash)
-
# #=> {"one" => "three"}
-
# Addressable::URI.parse("?").query_values
-
# #=> {}
-
# Addressable::URI.parse("").query_values
-
# #=> nil
-
1
def query_values(return_type=Hash)
-
empty_accumulator = Array == return_type ? [] : {}
-
if return_type != Hash && return_type != Array
-
raise ArgumentError, "Invalid return type. Must be Hash or Array."
-
end
-
return nil if self.query == nil
-
split_query = self.query.split("&").map do |pair|
-
pair.split("=", 2) if pair && !pair.empty?
-
end.compact
-
return split_query.inject(empty_accumulator.dup) do |accu, pair|
-
# I'd rather use key/value identifiers instead of array lookups,
-
# but in this case I really want to maintain the exact pair structure,
-
# so it's best to make all changes in-place.
-
pair[0] = URI.unencode_component(pair[0])
-
if pair[1].respond_to?(:to_str)
-
# I loathe the fact that I have to do this. Stupid HTML 4.01.
-
# Treating '+' as a space was just an unbelievably bad idea.
-
# There was nothing wrong with '%20'!
-
# If it ain't broke, don't fix it!
-
pair[1] = URI.unencode_component(pair[1].to_str.gsub(/\+/, " "))
-
end
-
if return_type == Hash
-
accu[pair[0]] = pair[1]
-
else
-
accu << pair
-
end
-
accu
-
end
-
end
-
-
##
-
# Sets the query component for this URI from a Hash object.
-
# An empty Hash or Array will result in an empty query string.
-
#
-
# @param [Hash, #to_hash, Array] new_query_values The new query values.
-
#
-
# @example
-
# uri.query_values = {:a => "a", :b => ["c", "d", "e"]}
-
# uri.query
-
# # => "a=a&b=c&b=d&b=e"
-
# uri.query_values = [['a', 'a'], ['b', 'c'], ['b', 'd'], ['b', 'e']]
-
# uri.query
-
# # => "a=a&b=c&b=d&b=e"
-
# uri.query_values = [['a', 'a'], ['b', ['c', 'd', 'e']]]
-
# uri.query
-
# # => "a=a&b=c&b=d&b=e"
-
# uri.query_values = [['flag'], ['key', 'value']]
-
# uri.query
-
# # => "flag&key=value"
-
1
def query_values=(new_query_values)
-
if new_query_values == nil
-
self.query = nil
-
return nil
-
end
-
-
if !new_query_values.is_a?(Array)
-
if !new_query_values.respond_to?(:to_hash)
-
raise TypeError,
-
"Can't convert #{new_query_values.class} into Hash."
-
end
-
new_query_values = new_query_values.to_hash
-
new_query_values = new_query_values.map do |key, value|
-
key = key.to_s if key.kind_of?(Symbol)
-
[key, value]
-
end
-
# Useful default for OAuth and caching.
-
# Only to be used for non-Array inputs. Arrays should preserve order.
-
new_query_values.sort!
-
end
-
-
# new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
-
buffer = ""
-
new_query_values.each do |key, value|
-
encoded_key = URI.encode_component(
-
key, CharacterClasses::UNRESERVED
-
)
-
if value == nil
-
buffer << "#{encoded_key}&"
-
elsif value.kind_of?(Array)
-
value.each do |sub_value|
-
encoded_value = URI.encode_component(
-
sub_value, CharacterClasses::UNRESERVED
-
)
-
buffer << "#{encoded_key}=#{encoded_value}&"
-
end
-
else
-
encoded_value = URI.encode_component(
-
value, CharacterClasses::UNRESERVED
-
)
-
buffer << "#{encoded_key}=#{encoded_value}&"
-
end
-
end
-
self.query = buffer.chop
-
end
-
-
##
-
# The HTTP request URI for this URI. This is the path and the
-
# query string.
-
#
-
# @return [String] The request URI required for an HTTP request.
-
1
def request_uri
-
return nil if self.absolute? && self.scheme !~ /^https?$/
-
return (
-
(!self.path.empty? ? self.path : SLASH) +
-
(self.query ? "?#{self.query}" : EMPTY_STR)
-
)
-
end
-
-
##
-
# Sets the HTTP request URI for this URI.
-
#
-
# @param [String, #to_str] new_request_uri The new HTTP request URI.
-
1
def request_uri=(new_request_uri)
-
if !new_request_uri.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_request_uri.class} into String."
-
end
-
if self.absolute? && self.scheme !~ /^https?$/
-
raise InvalidURIError,
-
"Cannot set an HTTP request URI for a non-HTTP URI."
-
end
-
new_request_uri = new_request_uri.to_str
-
path_component = new_request_uri[/^([^\?]*)\?(?:.*)$/, 1]
-
query_component = new_request_uri[/^(?:[^\?]*)\?(.*)$/, 1]
-
path_component = path_component.to_s
-
path_component = (!path_component.empty? ? path_component : SLASH)
-
self.path = path_component
-
self.query = query_component
-
-
# Reset dependent values
-
remove_composite_values
-
end
-
-
##
-
# The fragment component for this URI.
-
#
-
# @return [String] The fragment component.
-
1
def fragment
-
3
return defined?(@fragment) ? @fragment : nil
-
end
-
-
##
-
# The fragment component for this URI, normalized.
-
#
-
# @return [String] The fragment component, normalized.
-
1
def normalized_fragment
-
return nil unless self.fragment
-
return @normalized_fragment if defined?(@normalized_fragment)
-
@normalized_fragment ||= begin
-
component = Addressable::URI.normalize_component(
-
self.fragment,
-
Addressable::URI::CharacterClasses::FRAGMENT
-
)
-
component == "" ? nil : component
-
end
-
# All normalized values should be UTF-8
-
if @normalized_fragment
-
@normalized_fragment.force_encoding(Encoding::UTF_8)
-
end
-
@normalized_fragment
-
end
-
-
##
-
# Sets the fragment component for this URI.
-
#
-
# @param [String, #to_str] new_fragment The new fragment component.
-
1
def fragment=(new_fragment)
-
if new_fragment && !new_fragment.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{new_fragment.class} into String."
-
end
-
@fragment = new_fragment ? new_fragment.to_str : nil
-
-
# Reset dependent values
-
remove_instance_variable(:@normalized_fragment) if defined?(@normalized_fragment)
-
remove_composite_values
-
-
# Ensure we haven't created an invalid URI
-
validate()
-
end
-
-
##
-
# Determines if the scheme indicates an IP-based protocol.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the scheme indicates an IP-based protocol.
-
# <code>false</code> otherwise.
-
1
def ip_based?
-
2
if self.scheme
-
return URI.ip_based_schemes.include?(
-
2
self.scheme.strip.downcase)
-
end
-
return false
-
end
-
-
##
-
# Determines if the URI is relative.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the URI is relative. <code>false</code>
-
# otherwise.
-
1
def relative?
-
return self.scheme.nil?
-
end
-
-
##
-
# Determines if the URI is absolute.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the URI is absolute. <code>false</code>
-
# otherwise.
-
1
def absolute?
-
return !relative?
-
end
-
-
##
-
# Joins two URIs together.
-
#
-
# @param [String, Addressable::URI, #to_str] The URI to join with.
-
#
-
# @return [Addressable::URI] The joined URI.
-
1
def join(uri)
-
if !uri.respond_to?(:to_str)
-
raise TypeError, "Can't convert #{uri.class} into String."
-
end
-
if !uri.kind_of?(URI)
-
# Otherwise, convert to a String, then parse.
-
uri = URI.parse(uri.to_str)
-
end
-
if uri.to_s.empty?
-
return self.dup
-
end
-
-
joined_scheme = nil
-
joined_user = nil
-
joined_password = nil
-
joined_host = nil
-
joined_port = nil
-
joined_path = nil
-
joined_query = nil
-
joined_fragment = nil
-
-
# Section 5.2.2 of RFC 3986
-
if uri.scheme != nil
-
joined_scheme = uri.scheme
-
joined_user = uri.user
-
joined_password = uri.password
-
joined_host = uri.host
-
joined_port = uri.port
-
joined_path = URI.normalize_path(uri.path)
-
joined_query = uri.query
-
else
-
if uri.authority != nil
-
joined_user = uri.user
-
joined_password = uri.password
-
joined_host = uri.host
-
joined_port = uri.port
-
joined_path = URI.normalize_path(uri.path)
-
joined_query = uri.query
-
else
-
if uri.path == nil || uri.path.empty?
-
joined_path = self.path
-
if uri.query != nil
-
joined_query = uri.query
-
else
-
joined_query = self.query
-
end
-
else
-
if uri.path[0..0] == SLASH
-
joined_path = URI.normalize_path(uri.path)
-
else
-
base_path = self.path.dup
-
base_path = EMPTY_STR if base_path == nil
-
base_path = URI.normalize_path(base_path)
-
-
# Section 5.2.3 of RFC 3986
-
#
-
# Removes the right-most path segment from the base path.
-
if base_path =~ /\//
-
base_path.gsub!(/\/[^\/]+$/, SLASH)
-
else
-
base_path = EMPTY_STR
-
end
-
-
# If the base path is empty and an authority segment has been
-
# defined, use a base path of SLASH
-
if base_path.empty? && self.authority != nil
-
base_path = SLASH
-
end
-
-
joined_path = URI.normalize_path(base_path + uri.path)
-
end
-
joined_query = uri.query
-
end
-
joined_user = self.user
-
joined_password = self.password
-
joined_host = self.host
-
joined_port = self.port
-
end
-
joined_scheme = self.scheme
-
end
-
joined_fragment = uri.fragment
-
-
return self.class.new(
-
:scheme => joined_scheme,
-
:user => joined_user,
-
:password => joined_password,
-
:host => joined_host,
-
:port => joined_port,
-
:path => joined_path,
-
:query => joined_query,
-
:fragment => joined_fragment
-
)
-
end
-
1
alias_method :+, :join
-
-
##
-
# Destructive form of <code>join</code>.
-
#
-
# @param [String, Addressable::URI, #to_str] The URI to join with.
-
#
-
# @return [Addressable::URI] The joined URI.
-
#
-
# @see Addressable::URI#join
-
1
def join!(uri)
-
replace_self(self.join(uri))
-
end
-
-
##
-
# Merges a URI with a <code>Hash</code> of components.
-
# This method has different behavior from <code>join</code>. Any
-
# components present in the <code>hash</code> parameter will override the
-
# original components. The path component is not treated specially.
-
#
-
# @param [Hash, Addressable::URI, #to_hash] The components to merge with.
-
#
-
# @return [Addressable::URI] The merged URI.
-
#
-
# @see Hash#merge
-
1
def merge(hash)
-
if !hash.respond_to?(:to_hash)
-
raise TypeError, "Can't convert #{hash.class} into Hash."
-
end
-
hash = hash.to_hash
-
-
if hash.has_key?(:authority)
-
if (hash.keys & [:userinfo, :user, :password, :host, :port]).any?
-
raise ArgumentError,
-
"Cannot specify both an authority and any of the components " +
-
"within the authority."
-
end
-
end
-
if hash.has_key?(:userinfo)
-
if (hash.keys & [:user, :password]).any?
-
raise ArgumentError,
-
"Cannot specify both a userinfo and either the user or password."
-
end
-
end
-
-
uri = self.class.new
-
uri.defer_validation do
-
# Bunch of crazy logic required because of the composite components
-
# like userinfo and authority.
-
uri.scheme =
-
hash.has_key?(:scheme) ? hash[:scheme] : self.scheme
-
if hash.has_key?(:authority)
-
uri.authority =
-
hash.has_key?(:authority) ? hash[:authority] : self.authority
-
end
-
if hash.has_key?(:userinfo)
-
uri.userinfo =
-
hash.has_key?(:userinfo) ? hash[:userinfo] : self.userinfo
-
end
-
if !hash.has_key?(:userinfo) && !hash.has_key?(:authority)
-
uri.user =
-
hash.has_key?(:user) ? hash[:user] : self.user
-
uri.password =
-
hash.has_key?(:password) ? hash[:password] : self.password
-
end
-
if !hash.has_key?(:authority)
-
uri.host =
-
hash.has_key?(:host) ? hash[:host] : self.host
-
uri.port =
-
hash.has_key?(:port) ? hash[:port] : self.port
-
end
-
uri.path =
-
hash.has_key?(:path) ? hash[:path] : self.path
-
uri.query =
-
hash.has_key?(:query) ? hash[:query] : self.query
-
uri.fragment =
-
hash.has_key?(:fragment) ? hash[:fragment] : self.fragment
-
end
-
-
return uri
-
end
-
-
##
-
# Destructive form of <code>merge</code>.
-
#
-
# @param [Hash, Addressable::URI, #to_hash] The components to merge with.
-
#
-
# @return [Addressable::URI] The merged URI.
-
#
-
# @see Addressable::URI#merge
-
1
def merge!(uri)
-
replace_self(self.merge(uri))
-
end
-
-
##
-
# Returns the shortest normalized relative form of this URI that uses the
-
# supplied URI as a base for resolution. Returns an absolute URI if
-
# necessary. This is effectively the opposite of <code>route_to</code>.
-
#
-
# @param [String, Addressable::URI, #to_str] uri The URI to route from.
-
#
-
# @return [Addressable::URI]
-
# The normalized relative URI that is equivalent to the original URI.
-
1
def route_from(uri)
-
uri = URI.parse(uri).normalize
-
normalized_self = self.normalize
-
if normalized_self.relative?
-
raise ArgumentError, "Expected absolute URI, got: #{self.to_s}"
-
end
-
if uri.relative?
-
raise ArgumentError, "Expected absolute URI, got: #{uri.to_s}"
-
end
-
if normalized_self == uri
-
return Addressable::URI.parse("##{normalized_self.fragment}")
-
end
-
components = normalized_self.to_hash
-
if normalized_self.scheme == uri.scheme
-
components[:scheme] = nil
-
if normalized_self.authority == uri.authority
-
components[:user] = nil
-
components[:password] = nil
-
components[:host] = nil
-
components[:port] = nil
-
if normalized_self.path == uri.path
-
components[:path] = nil
-
if normalized_self.query == uri.query
-
components[:query] = nil
-
end
-
else
-
if uri.path != SLASH and components[:path]
-
self_splitted_path = split_path(components[:path])
-
uri_splitted_path = split_path(uri.path)
-
self_dir = self_splitted_path.shift
-
uri_dir = uri_splitted_path.shift
-
while !self_splitted_path.empty? && !uri_splitted_path.empty? and self_dir == uri_dir
-
self_dir = self_splitted_path.shift
-
uri_dir = uri_splitted_path.shift
-
end
-
components[:path] = (uri_splitted_path.fill('..') + [self_dir] + self_splitted_path).join(SLASH)
-
end
-
end
-
end
-
end
-
# Avoid network-path references.
-
if components[:host] != nil
-
components[:scheme] = normalized_self.scheme
-
end
-
return Addressable::URI.new(
-
:scheme => components[:scheme],
-
:user => components[:user],
-
:password => components[:password],
-
:host => components[:host],
-
:port => components[:port],
-
:path => components[:path],
-
:query => components[:query],
-
:fragment => components[:fragment]
-
)
-
end
-
-
##
-
# Returns the shortest normalized relative form of the supplied URI that
-
# uses this URI as a base for resolution. Returns an absolute URI if
-
# necessary. This is effectively the opposite of <code>route_from</code>.
-
#
-
# @param [String, Addressable::URI, #to_str] uri The URI to route to.
-
#
-
# @return [Addressable::URI]
-
# The normalized relative URI that is equivalent to the supplied URI.
-
1
def route_to(uri)
-
return URI.parse(uri).route_from(self)
-
end
-
-
##
-
# Returns a normalized URI object.
-
#
-
# NOTE: This method does not attempt to fully conform to specifications.
-
# It exists largely to correct other people's failures to read the
-
# specifications, and also to deal with caching issues since several
-
# different URIs may represent the same resource and should not be
-
# cached multiple times.
-
#
-
# @return [Addressable::URI] The normalized URI.
-
1
def normalize
-
# This is a special exception for the frequently misused feed
-
# URI scheme.
-
if normalized_scheme == "feed"
-
if self.to_s =~ /^feed:\/*http:\/*/
-
return URI.parse(
-
self.to_s[/^feed:\/*(http:\/*.*)/, 1]
-
).normalize
-
end
-
end
-
-
return self.class.new(
-
:scheme => normalized_scheme,
-
:authority => normalized_authority,
-
:path => normalized_path,
-
:query => normalized_query,
-
:fragment => normalized_fragment
-
)
-
end
-
-
##
-
# Destructively normalizes this URI object.
-
#
-
# @return [Addressable::URI] The normalized URI.
-
#
-
# @see Addressable::URI#normalize
-
1
def normalize!
-
replace_self(self.normalize)
-
end
-
-
##
-
# Creates a URI suitable for display to users. If semantic attacks are
-
# likely, the application should try to detect these and warn the user.
-
# See <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>,
-
# section 7.6 for more information.
-
#
-
# @return [Addressable::URI] A URI suitable for display purposes.
-
1
def display_uri
-
display_uri = self.normalize
-
display_uri.host = ::Addressable::IDNA.to_unicode(display_uri.host)
-
return display_uri
-
end
-
-
##
-
# Returns <code>true</code> if the URI objects are equal. This method
-
# normalizes both URIs before doing the comparison, and allows comparison
-
# against <code>Strings</code>.
-
#
-
# @param [Object] uri The URI to compare.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the URIs are equivalent, <code>false</code>
-
# otherwise.
-
1
def ===(uri)
-
if uri.respond_to?(:normalize)
-
uri_string = uri.normalize.to_s
-
else
-
begin
-
uri_string = ::Addressable::URI.parse(uri).normalize.to_s
-
rescue InvalidURIError, TypeError
-
return false
-
end
-
end
-
return self.normalize.to_s == uri_string
-
end
-
-
##
-
# Returns <code>true</code> if the URI objects are equal. This method
-
# normalizes both URIs before doing the comparison.
-
#
-
# @param [Object] uri The URI to compare.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the URIs are equivalent, <code>false</code>
-
# otherwise.
-
1
def ==(uri)
-
return false unless uri.kind_of?(URI)
-
return self.normalize.to_s == uri.normalize.to_s
-
end
-
-
##
-
# Returns <code>true</code> if the URI objects are equal. This method
-
# does NOT normalize either URI before doing the comparison.
-
#
-
# @param [Object] uri The URI to compare.
-
#
-
# @return [TrueClass, FalseClass]
-
# <code>true</code> if the URIs are equivalent, <code>false</code>
-
# otherwise.
-
1
def eql?(uri)
-
return false unless uri.kind_of?(URI)
-
return self.to_s == uri.to_s
-
end
-
-
##
-
# A hash value that will make a URI equivalent to its normalized
-
# form.
-
#
-
# @return [Integer] A hash of the URI.
-
1
def hash
-
@hash ||= self.to_s.hash * -1
-
end
-
-
##
-
# Clones the URI object.
-
#
-
# @return [Addressable::URI] The cloned URI.
-
1
def dup
-
duplicated_uri = self.class.new(
-
:scheme => self.scheme ? self.scheme.dup : nil,
-
:user => self.user ? self.user.dup : nil,
-
:password => self.password ? self.password.dup : nil,
-
:host => self.host ? self.host.dup : nil,
-
:port => self.port,
-
:path => self.path ? self.path.dup : nil,
-
:query => self.query ? self.query.dup : nil,
-
:fragment => self.fragment ? self.fragment.dup : nil
-
)
-
return duplicated_uri
-
end
-
-
##
-
# Omits components from a URI.
-
#
-
# @param [Symbol] *components The components to be omitted.
-
#
-
# @return [Addressable::URI] The URI with components omitted.
-
#
-
# @example
-
# uri = Addressable::URI.parse("http://example.com/path?query")
-
# #=> #<Addressable::URI:0xcc5e7a URI:http://example.com/path?query>
-
# uri.omit(:scheme, :authority)
-
# #=> #<Addressable::URI:0xcc4d86 URI:/path?query>
-
1
def omit(*components)
-
invalid_components = components - [
-
:scheme, :user, :password, :userinfo, :host, :port, :authority,
-
:path, :query, :fragment
-
]
-
unless invalid_components.empty?
-
raise ArgumentError,
-
"Invalid component names: #{invalid_components.inspect}."
-
end
-
duplicated_uri = self.dup
-
duplicated_uri.defer_validation do
-
components.each do |component|
-
duplicated_uri.send((component.to_s + "=").to_sym, nil)
-
end
-
duplicated_uri.user = duplicated_uri.normalized_user
-
end
-
duplicated_uri
-
end
-
-
##
-
# Destructive form of omit.
-
#
-
# @param [Symbol] *components The components to be omitted.
-
#
-
# @return [Addressable::URI] The URI with components omitted.
-
#
-
# @see Addressable::URI#omit
-
1
def omit!(*components)
-
replace_self(self.omit(*components))
-
end
-
-
##
-
# Determines if the URI is an empty string.
-
#
-
# @return [TrueClass, FalseClass]
-
# Returns <code>true</code> if empty, <code>false</code> otherwise.
-
1
def empty?
-
return self.to_s.empty?
-
end
-
-
##
-
# Converts the URI to a <code>String</code>.
-
#
-
# @return [String] The URI's <code>String</code> representation.
-
1
def to_s
-
2
if self.scheme == nil && self.path != nil && !self.path.empty? &&
-
self.path =~ NORMPATH
-
raise InvalidURIError,
-
"Cannot assemble URI string with ambiguous path: '#{self.path}'"
-
end
-
@uri_string ||= begin
-
2
uri_string = String.new
-
2
uri_string << "#{self.scheme}:" if self.scheme != nil
-
2
uri_string << "//#{self.authority}" if self.authority != nil
-
2
uri_string << self.path.to_s
-
2
uri_string << "?#{self.query}" if self.query != nil
-
2
uri_string << "##{self.fragment}" if self.fragment != nil
-
2
uri_string.force_encoding(Encoding::UTF_8)
-
2
uri_string
-
2
end
-
end
-
-
##
-
# URI's are glorified <code>Strings</code>. Allow implicit conversion.
-
1
alias_method :to_str, :to_s
-
-
##
-
# Returns a Hash of the URI components.
-
#
-
# @return [Hash] The URI as a <code>Hash</code> of components.
-
1
def to_hash
-
return {
-
:scheme => self.scheme,
-
:user => self.user,
-
:password => self.password,
-
:host => self.host,
-
:port => self.port,
-
:path => self.path,
-
:query => self.query,
-
:fragment => self.fragment
-
1
}
-
end
-
-
##
-
# Returns a <code>String</code> representation of the URI object's state.
-
#
-
# @return [String] The URI object's state, as a <code>String</code>.
-
1
def inspect
-
sprintf("#<%s:%#0x URI:%s>", URI.to_s, self.object_id, self.to_s)
-
end
-
-
##
-
# This method allows you to make several changes to a URI simultaneously,
-
# which separately would cause validation errors, but in conjunction,
-
# are valid. The URI will be revalidated as soon as the entire block has
-
# been executed.
-
#
-
# @param [Proc] block
-
# A set of operations to perform on a given URI.
-
1
def defer_validation(&block)
-
2
raise LocalJumpError, "No block given." unless block
-
2
@validation_deferred = true
-
2
block.call()
-
2
@validation_deferred = false
-
2
validate
-
return nil
-
end
-
-
1
protected
-
1
SELF_REF = '.'
-
1
PARENT = '..'
-
-
1
RULE_2A = /\/\.\/|\/\.$/
-
1
RULE_2B_2C = /\/([^\/]*)\/\.\.\/|\/([^\/]*)\/\.\.$/
-
1
RULE_2D = /^\.\.?\/?/
-
1
RULE_PREFIXED_PARENT = /^\/\.\.?\/|^(\/\.\.?)+\/?$/
-
-
##
-
# Resolves paths to their simplest form.
-
#
-
# @param [String] path The path to normalize.
-
#
-
# @return [String] The normalized path.
-
1
def self.normalize_path(path)
-
# Section 5.2.4 of RFC 3986
-
-
return nil if path.nil?
-
normalized_path = path.dup
-
begin
-
mod = nil
-
mod ||= normalized_path.gsub!(RULE_2A, SLASH)
-
-
pair = normalized_path.match(RULE_2B_2C)
-
parent, current = pair[1], pair[2] if pair
-
if pair && ((parent != SELF_REF && parent != PARENT) ||
-
(current != SELF_REF && current != PARENT))
-
mod ||= normalized_path.gsub!(
-
Regexp.new(
-
"/#{Regexp.escape(parent.to_s)}/\\.\\./|" +
-
"(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
-
), SLASH
-
)
-
end
-
-
mod ||= normalized_path.gsub!(RULE_2D, EMPTY_STR)
-
# Non-standard, removes prefixed dotted segments from path.
-
mod ||= normalized_path.gsub!(RULE_PREFIXED_PARENT, SLASH)
-
end until mod.nil?
-
-
return normalized_path
-
end
-
-
##
-
# Ensures that the URI is valid.
-
1
def validate
-
9
return if !!@validation_deferred
-
2
if self.scheme != nil && self.ip_based? &&
-
(self.host == nil || self.host.empty?) &&
-
(self.path == nil || self.path.empty?)
-
raise InvalidURIError,
-
"Absolute URI missing hierarchical segment: '#{self.to_s}'"
-
end
-
2
if self.host == nil
-
if self.port != nil ||
-
self.user != nil ||
-
self.password != nil
-
raise InvalidURIError, "Hostname not supplied: '#{self.to_s}'"
-
end
-
end
-
2
if self.path != nil && !self.path.empty? && self.path[0..0] != SLASH &&
-
self.authority != nil
-
raise InvalidURIError,
-
"Cannot have a relative path with an authority set: '#{self.to_s}'"
-
end
-
2
if self.path != nil && !self.path.empty? &&
-
self.path[0..1] == SLASH + SLASH && self.authority == nil
-
raise InvalidURIError,
-
"Cannot have a path with two leading slashes " +
-
"without an authority set: '#{self.to_s}'"
-
end
-
2
unreserved = CharacterClasses::UNRESERVED
-
2
sub_delims = CharacterClasses::SUB_DELIMS
-
if !self.host.nil? && (self.host =~ /[<>{}\/\\\?\#\@"[[:space:]]]/ ||
-
(self.host[/^\[(.*)\]$/, 1] != nil && self.host[/^\[(.*)\]$/, 1] !~
-
2
Regexp.new("^[#{unreserved}#{sub_delims}:]*$")))
-
raise InvalidURIError, "Invalid character in host: '#{self.host.to_s}'"
-
end
-
return nil
-
end
-
-
##
-
# Replaces the internal state of self with the specified URI's state.
-
# Used in destructive operations to avoid massive code repetition.
-
#
-
# @param [Addressable::URI] uri The URI to replace <code>self</code> with.
-
#
-
# @return [Addressable::URI] <code>self</code>.
-
1
def replace_self(uri)
-
# Reset dependent values
-
instance_variables.each do |var|
-
if instance_variable_defined?(var) && var != :@validation_deferred
-
remove_instance_variable(var)
-
end
-
end
-
-
@scheme = uri.scheme
-
@user = uri.user
-
@password = uri.password
-
@host = uri.host
-
@port = uri.port
-
@path = uri.path
-
@query = uri.query
-
@fragment = uri.fragment
-
return self
-
end
-
-
##
-
# Splits path string with "/" (slash).
-
# It is considered that there is empty string after last slash when
-
# path ends with slash.
-
#
-
# @param [String] path The path to split.
-
#
-
# @return [Array<String>] An array of parts of path.
-
1
def split_path(path)
-
splitted = path.split(SLASH)
-
splitted << EMPTY_STR if path.end_with? SLASH
-
splitted
-
end
-
-
##
-
# Resets composite values for the entire URI
-
#
-
# @api private
-
1
def remove_composite_values
-
7
remove_instance_variable(:@uri_string) if defined?(@uri_string)
-
7
remove_instance_variable(:@hash) if defined?(@hash)
-
end
-
end
-
end
-
# encoding:utf-8
-
#--
-
# Copyright (C) Bob Aman
-
#
-
# Licensed under the Apache License, Version 2.0 (the "License");
-
# you may not use this file except in compliance with the License.
-
# You may obtain a copy of the License at
-
#
-
# http://www.apache.org/licenses/LICENSE-2.0
-
#
-
# Unless required by applicable law or agreed to in writing, software
-
# distributed under the License is distributed on an "AS IS" BASIS,
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
# See the License for the specific language governing permissions and
-
# limitations under the License.
-
#++
-
-
-
# Used to prevent the class/module from being loaded more than once
-
1
if !defined?(Addressable::VERSION)
-
1
module Addressable
-
1
module VERSION
-
1
MAJOR = 2
-
1
MINOR = 5
-
1
TINY = 0
-
-
1
STRING = [MAJOR, MINOR, TINY].join('.')
-
end
-
end
-
end
-
# A Ruby library implementing OpenBSD's bcrypt()/crypt_blowfish algorithm for
-
# hashing passwords.
-
1
module BCrypt
-
end
-
-
1
if RUBY_PLATFORM == "java"
-
require 'java'
-
else
-
1
require "openssl"
-
end
-
-
1
begin
-
1
RUBY_VERSION =~ /(\d+.\d+)/
-
1
require "#{$1}/bcrypt_ext"
-
rescue LoadError
-
1
require "bcrypt_ext"
-
end
-
-
1
require 'bcrypt/error'
-
1
require 'bcrypt/engine'
-
1
require 'bcrypt/password'
-
1
module BCrypt
-
# A Ruby wrapper for the bcrypt() C extension calls and the Java calls.
-
1
class Engine
-
# The default computational expense parameter.
-
1
DEFAULT_COST = 10
-
# The minimum cost supported by the algorithm.
-
1
MIN_COST = 4
-
# Maximum possible size of bcrypt() salts.
-
1
MAX_SALT_LENGTH = 16
-
-
1
if RUBY_PLATFORM != "java"
-
# C-level routines which, if they don't get the right input, will crash the
-
# hell out of the Ruby process.
-
1
private_class_method :__bc_salt
-
1
private_class_method :__bc_crypt
-
end
-
-
1
@cost = nil
-
-
# Returns the cost factor that will be used if one is not specified when
-
# creating a password hash. Defaults to DEFAULT_COST if not set.
-
1
def self.cost
-
11
@cost || DEFAULT_COST
-
end
-
-
# Set a default cost factor that will be used if one is not specified when
-
# creating a password hash.
-
#
-
# Example:
-
#
-
# BCrypt::Engine::DEFAULT_COST #=> 10
-
# BCrypt::Password.create('secret').cost #=> 10
-
#
-
# BCrypt::Engine.cost = 8
-
# BCrypt::Password.create('secret').cost #=> 8
-
#
-
# # cost can still be overridden as needed
-
# BCrypt::Password.create('secret', :cost => 6).cost #=> 6
-
1
def self.cost=(cost)
-
@cost = cost
-
end
-
-
# Given a secret and a valid salt (see BCrypt::Engine.generate_salt) calculates
-
# a bcrypt() password hash.
-
1
def self.hash_secret(secret, salt, _ = nil)
-
12
if valid_secret?(secret)
-
12
if valid_salt?(salt)
-
12
if RUBY_PLATFORM == "java"
-
Java.bcrypt_jruby.BCrypt.hashpw(secret.to_s, salt.to_s)
-
else
-
12
__bc_crypt(secret.to_s, salt)
-
end
-
else
-
raise Errors::InvalidSalt.new("invalid salt")
-
end
-
else
-
raise Errors::InvalidSecret.new("invalid secret")
-
end
-
end
-
-
# Generates a random salt with a given computational cost.
-
1
def self.generate_salt(cost = self.cost)
-
11
cost = cost.to_i
-
11
if cost > 0
-
11
if cost < MIN_COST
-
cost = MIN_COST
-
end
-
11
if RUBY_PLATFORM == "java"
-
Java.bcrypt_jruby.BCrypt.gensalt(cost)
-
else
-
11
prefix = "$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW"
-
11
__bc_salt(prefix, cost, OpenSSL::Random.random_bytes(MAX_SALT_LENGTH))
-
end
-
else
-
raise Errors::InvalidCost.new("cost must be numeric and > 0")
-
end
-
end
-
-
# Returns true if +salt+ is a valid bcrypt() salt, false if not.
-
1
def self.valid_salt?(salt)
-
12
!!(salt =~ /^\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}$/)
-
end
-
-
# Returns true if +secret+ is a valid bcrypt() secret, false if not.
-
1
def self.valid_secret?(secret)
-
12
secret.respond_to?(:to_s)
-
end
-
-
# Returns the cost factor which will result in computation times less than +upper_time_limit_in_ms+.
-
#
-
# Example:
-
#
-
# BCrypt::Engine.calibrate(200) #=> 10
-
# BCrypt::Engine.calibrate(1000) #=> 12
-
#
-
# # should take less than 200ms
-
# BCrypt::Password.create("woo", :cost => 10)
-
#
-
# # should take less than 1000ms
-
# BCrypt::Password.create("woo", :cost => 12)
-
1
def self.calibrate(upper_time_limit_in_ms)
-
40.times do |i|
-
start_time = Time.now
-
Password.create("testing testing", :cost => i+1)
-
end_time = Time.now - start_time
-
return i if end_time * 1_000 > upper_time_limit_in_ms
-
end
-
end
-
-
# Autodetects the cost from the salt string.
-
1
def self.autodetect_cost(salt)
-
salt[4..5].to_i
-
end
-
end
-
-
end
-
1
module BCrypt
-
-
1
class Error < StandardError # :nodoc:
-
end
-
-
1
module Errors # :nodoc:
-
-
# The salt parameter provided to bcrypt() is invalid.
-
1
class InvalidSalt < BCrypt::Error; end
-
-
# The hash parameter provided to bcrypt() is invalid.
-
1
class InvalidHash < BCrypt::Error; end
-
-
# The cost parameter provided to bcrypt() is invalid.
-
1
class InvalidCost < BCrypt::Error; end
-
-
# The secret parameter provided to bcrypt() is invalid.
-
1
class InvalidSecret < BCrypt::Error; end
-
-
end
-
-
end
-
1
module BCrypt
-
# A password management class which allows you to safely store users' passwords and compare them.
-
#
-
# Example usage:
-
#
-
# include BCrypt
-
#
-
# # hash a user's password
-
# @password = Password.create("my grand secret")
-
# @password #=> "$2a$10$GtKs1Kbsig8ULHZzO1h2TetZfhO4Fmlxphp8bVKnUlZCBYYClPohG"
-
#
-
# # store it safely
-
# @user.update_attribute(:password, @password)
-
#
-
# # read it back
-
# @user.reload!
-
# @db_password = Password.new(@user.password)
-
#
-
# # compare it after retrieval
-
# @db_password == "my grand secret" #=> true
-
# @db_password == "a paltry guess" #=> false
-
#
-
1
class Password < String
-
# The hash portion of the stored password hash.
-
1
attr_reader :checksum
-
# The salt of the store password hash (including version and cost).
-
1
attr_reader :salt
-
# The version of the bcrypt() algorithm used to create the hash.
-
1
attr_reader :version
-
# The cost factor used to create the hash.
-
1
attr_reader :cost
-
-
1
class << self
-
# Hashes a secret, returning a BCrypt::Password instance. Takes an optional <tt>:cost</tt> option, which is a
-
# logarithmic variable which determines how computational expensive the hash is to calculate (a <tt>:cost</tt> of
-
# 4 is twice as much work as a <tt>:cost</tt> of 3). The higher the <tt>:cost</tt> the harder it becomes for
-
# attackers to try to guess passwords (even if a copy of your database is stolen), but the slower it is to check
-
# users' passwords.
-
#
-
# Example:
-
#
-
# @password = BCrypt::Password.create("my secret", :cost => 13)
-
1
def create(secret, options = {})
-
11
cost = options[:cost] || BCrypt::Engine.cost
-
11
raise ArgumentError if cost > 31
-
11
Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)))
-
end
-
-
1
def valid_hash?(h)
-
12
h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/
-
end
-
end
-
-
# Initializes a BCrypt::Password instance with the data from a stored hash.
-
1
def initialize(raw_hash)
-
12
if valid_hash?(raw_hash)
-
12
self.replace(raw_hash)
-
12
@version, @cost, @salt, @checksum = split_hash(self)
-
else
-
raise Errors::InvalidHash.new("invalid hash")
-
end
-
end
-
-
# Compares a potential secret against the hash. Returns true if the secret is the original secret, false otherwise.
-
1
def ==(secret)
-
1
super(BCrypt::Engine.hash_secret(secret, @salt))
-
end
-
1
alias_method :is_password?, :==
-
-
1
private
-
-
# Returns true if +h+ is a valid hash.
-
1
def valid_hash?(h)
-
12
self.class.valid_hash?(h)
-
end
-
-
# call-seq:
-
# split_hash(raw_hash) -> version, cost, salt, hash
-
#
-
# Splits +h+ into version, cost, salt, and hash and returns them in that order.
-
1
def split_hash(h)
-
12
_, v, c, mash = h.split('$')
-
12
return v.to_str, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'timeout'
-
1
require 'nokogiri'
-
1
require 'xpath'
-
-
1
module Capybara
-
1
class CapybaraError < StandardError; end
-
1
class DriverNotFoundError < CapybaraError; end
-
1
class FrozenInTime < CapybaraError; end
-
1
class ElementNotFound < CapybaraError; end
-
1
class ModalNotFound < CapybaraError; end
-
1
class Ambiguous < ElementNotFound; end
-
1
class ExpectationNotMet < ElementNotFound; end
-
1
class FileNotFound < CapybaraError; end
-
1
class UnselectNotAllowed < CapybaraError; end
-
1
class NotSupportedByDriverError < CapybaraError; end
-
1
class InfiniteRedirectError < CapybaraError; end
-
1
class ScopeError < CapybaraError; end
-
1
class WindowError < CapybaraError; end
-
1
class ReadOnlyElementError < CapybaraError; end
-
-
1
class << self
-
1
attr_reader :app_host, :default_host
-
1
attr_accessor :asset_host, :run_server, :always_include_port
-
1
attr_accessor :server_port, :exact, :match, :exact_options, :visible_text_only, :enable_aria_label
-
1
attr_accessor :default_selector, :default_max_wait_time, :ignore_hidden_elements
-
1
attr_accessor :save_path, :wait_on_first_by_default, :automatic_label_click, :automatic_reload
-
1
attr_reader :reuse_server
-
1
attr_accessor :raise_server_errors, :server_errors
-
1
attr_writer :default_driver, :current_driver, :javascript_driver, :session_name, :server_host
-
1
attr_reader :save_and_open_page_path
-
1
attr_accessor :exact_text
-
1
attr_accessor :app
-
-
##
-
#
-
# Configure Capybara to suit your needs.
-
#
-
# Capybara.configure do |config|
-
# config.run_server = false
-
# config.app_host = 'http://www.google.com'
-
# end
-
#
-
# === Configurable options
-
#
-
# [app_host = String/nil] The default host to use when giving a relative URL to visit, must be a valid URL e.g. http://www.example.com
-
# [always_include_port = Boolean] Whether the Rack server's port should automatically be inserted into every visited URL (Default: false)
-
# [asset_host = String] Where dynamic assets are hosted - will be prepended to relative asset locations if present (Default: nil)
-
# [run_server = Boolean] Whether to start a Rack server for the given Rack app (Default: true)
-
# [raise_server_errors = Boolean] Should errors raised in the server be raised in the tests? (Default: true)
-
# [server_errors = Array\<Class\>] Error classes that should be raised in the tests if they are raised in the server and Capybara.raise_server_errors is true (Default: [StandardError])
-
# [default_selector = :css/:xpath] Methods which take a selector use the given type by default (Default: :css)
-
# [default_max_wait_time = Numeric] The maximum number of seconds to wait for asynchronous processes to finish (Default: 2)
-
# [ignore_hidden_elements = Boolean] Whether to ignore hidden elements on the page (Default: true)
-
# [automatic_reload = Boolean] Whether to automatically reload elements as Capybara is waiting (Default: true)
-
# [save_path = String] Where to put pages saved through save_(page|screenshot), save_and_open_(page|screenshot) (Default: Dir.pwd)
-
# [wait_on_first_by_default = Boolean] Whether Node#first defaults to Capybara waiting behavior for at least 1 element to match (Default: false)
-
# [automatic_label_click = Boolean] Whether Node#choose, Node#check, Node#uncheck will attempt to click the associated label element if the checkbox/radio button are non-visible (Default: false)
-
# [enable_aria_label = Boolean] Whether fields, links, and buttons will match against aria-label attribute (Default: false)
-
# [reuse_server = Boolean] Reuse the server thread between multiple sessions using the same app object (Default: true)
-
# === DSL Options
-
#
-
# when using capybara/dsl, the following options are also available:
-
#
-
# [default_driver = Symbol] The name of the driver to use by default. (Default: :rack_test)
-
# [javascript_driver = Symbol] The name of a driver to use for JavaScript enabled tests. (Default: :selenium)
-
#
-
1
def configure
-
1
yield self
-
end
-
-
##
-
#
-
# Register a new driver for Capybara.
-
#
-
# Capybara.register_driver :rack_test do |app|
-
# Capybara::RackTest::Driver.new(app)
-
# end
-
#
-
# @param [Symbol] name The name of the new driver
-
# @yield [app] This block takes a rack app and returns a Capybara driver
-
# @yieldparam [<Rack>] app The rack application that this driver runs against. May be nil.
-
# @yieldreturn [Capybara::Driver::Base] A Capybara driver instance
-
#
-
1
def register_driver(name, &block)
-
3
drivers[name] = block
-
end
-
-
##
-
#
-
# Register a new server for Capybara.
-
#
-
# Capybara.register_server :webrick do |app, port, host|
-
# require 'rack/handler/webrick'
-
# Rack::Handler::WEBrick.run(app, ...)
-
# end
-
#
-
# @param [Symbol] name The name of the new driver
-
# @yield [app, port, host] This block takes a rack app and a port and returns a rack server listening on that port
-
# @yieldparam [<Rack>] app The rack application that this server will contain.
-
# @yieldparam port The port number the server should listen on
-
# @yieldparam host The host/ip to bind to
-
# @yieldreturn [Capybara::Driver::Base] A Capybara driver instance
-
#
-
1
def register_server(name, &block)
-
3
servers[name.to_sym] = block
-
end
-
-
##
-
#
-
# Add a new selector to Capybara. Selectors can be used by various methods in Capybara
-
# to find certain elements on the page in a more convenient way. For example adding a
-
# selector to find certain table rows might look like this:
-
#
-
# Capybara.add_selector(:row) do
-
# xpath { |num| ".//tbody/tr[#{num}]" }
-
# end
-
#
-
# This makes it possible to use this selector in a variety of ways:
-
#
-
# find(:row, 3)
-
# page.find('table#myTable').find(:row, 3).text
-
# page.find('table#myTable').has_selector?(:row, 3)
-
# within(:row, 3) { expect(page).to have_content('$100.000') }
-
#
-
# Here is another example:
-
#
-
# Capybara.add_selector(:id) do
-
# xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
-
# end
-
#
-
# Note that this particular selector already ships with Capybara.
-
#
-
# @param [Symbol] name The name of the selector to add
-
# @yield A block executed in the context of the new {Capybara::Selector}
-
#
-
1
def add_selector(name, &block)
-
17
Capybara::Selector.add(name, &block)
-
end
-
-
##
-
#
-
# Modify a selector previously created by {Capybara.add_selector}.
-
# For example modifying the :button selector to also find divs styled
-
# to look like buttons might look like this
-
#
-
# Capybara.modify_selector(:button) do
-
# xpath { |locator| XPath::HTML.button(locator).or(XPath::css('div.btn')[XPath::string.n.is(locator)]) }
-
# end
-
#
-
# @param [Symbol] name The name of the selector to modify
-
# @yield A block executed in the context of the existing {Capybara::Selector}
-
#
-
1
def modify_selector(name, &block)
-
Capybara::Selector.update(name, &block)
-
end
-
-
1
def drivers
-
5
@drivers ||= {}
-
end
-
-
1
def servers
-
5
@servers ||= {}
-
end
-
-
##
-
#
-
# Register a proc that Capybara will call to run the Rack application.
-
#
-
# Capybara.server do |app, port, host|
-
# require 'rack/handler/mongrel'
-
# Rack::Handler::Mongrel.run(app, :Port => port)
-
# end
-
#
-
# By default, Capybara will try to run webrick.
-
#
-
# @yield [app, port, host] This block receives a rack app, port, and host/ip and should run a Rack handler
-
#
-
1
def server(&block)
-
1
if block_given?
-
warn "DEPRECATED: Passing a block to Capybara::server is deprecated, please use Capybara::register_server instead"
-
@server = block
-
else
-
1
@server
-
end
-
end
-
-
##
-
#
-
# Set the server to use.
-
#
-
# Capybara.server = :webrick
-
#
-
# @param [Symbol] name Name of the server type to use
-
# @see register_server
-
#
-
1
def server=(name)
-
1
@server = if name.respond_to? :call
-
name
-
else
-
1
servers[name.to_sym]
-
end
-
end
-
-
##
-
#
-
# Wraps the given string, which should contain an HTML document or fragment
-
# in a {Capybara::Node::Simple} which exposes all {Capybara::Node::Matchers},
-
# {Capybara::Node::Finders} and {Capybara::Node::DocumentMatchers}. This allows you to query
-
# any string containing HTML in the exact same way you would query the current document in a Capybara
-
# session.
-
#
-
# Example: A single element
-
#
-
# node = Capybara.string('<a href="foo">bar</a>')
-
# anchor = node.first('a')
-
# anchor[:href] #=> 'foo'
-
# anchor.text #=> 'bar'
-
#
-
# Example: Multiple elements
-
#
-
# node = Capybara.string <<-HTML
-
# <ul>
-
# <li id="home">Home</li>
-
# <li id="projects">Projects</li>
-
# </ul>
-
# HTML
-
#
-
# node.find('#projects').text # => 'Projects'
-
# node.has_selector?('li#home', text: 'Home')
-
# node.has_selector?('#projects')
-
# node.find('ul').find('li:first-child').text # => 'Home'
-
#
-
# @param [String] html An html fragment or document
-
# @return [Capybara::Node::Simple] A node which has Capybara's finders and matchers
-
#
-
1
def string(html)
-
Capybara::Node::Simple.new(html)
-
end
-
-
##
-
#
-
# Runs Capybara's default server for the given application and port
-
# under most circumstances you should not have to call this method
-
# manually.
-
#
-
# @param [Rack Application] app The rack application to run
-
# @param [Integer] port The port to run the application on
-
#
-
1
def run_default_server(app, port)
-
1
servers[:webrick].call(app, port, server_host)
-
end
-
-
##
-
#
-
# @return [Symbol] The name of the driver to use by default
-
#
-
1
def default_driver
-
95
@default_driver || :rack_test
-
end
-
-
##
-
#
-
# @return [Symbol] The name of the driver currently in use
-
#
-
1
def current_driver
-
95
@current_driver || default_driver
-
end
-
1
alias_method :mode, :current_driver
-
-
##
-
#
-
# @return [Symbol] The name of the driver used when JavaScript is needed
-
#
-
1
def javascript_driver
-
@javascript_driver || :selenium
-
end
-
-
##
-
#
-
# Use the default driver as the current driver
-
#
-
1
def use_default_driver
-
14
@current_driver = nil
-
end
-
-
##
-
#
-
# Yield a block using a specific driver
-
#
-
1
def using_driver(driver)
-
previous_driver = Capybara.current_driver
-
Capybara.current_driver = driver
-
yield
-
ensure
-
@current_driver = previous_driver
-
end
-
-
##
-
#
-
# @return [String] The IP address bound by default server
-
#
-
1
def server_host
-
2
@server_host || '127.0.0.1'
-
end
-
-
##
-
#
-
# Yield a block using a specific wait time
-
#
-
1
def using_wait_time(seconds)
-
previous_wait_time = Capybara.default_max_wait_time
-
Capybara.default_max_wait_time = seconds
-
yield
-
ensure
-
Capybara.default_max_wait_time = previous_wait_time
-
end
-
-
##
-
#
-
# The current Capybara::Session based on what is set as Capybara.app and Capybara.current_driver
-
#
-
# @return [Capybara::Session] The currently used session
-
#
-
1
def current_session
-
94
session_pool["#{current_driver}:#{session_name}:#{app.object_id}"] ||= Capybara::Session.new(current_driver, app)
-
end
-
-
##
-
#
-
# Reset sessions, cleaning out the pool of sessions. This will remove any session information such
-
# as cookies.
-
#
-
1
def reset_sessions!
-
#reset in reverse so sessions that started servers are reset last
-
28
session_pool.reverse_each { |_mode, session| session.reset! }
-
end
-
1
alias_method :reset!, :reset_sessions!
-
-
##
-
#
-
# The current session name.
-
#
-
# @return [Symbol] The name of the currently used session.
-
#
-
1
def session_name
-
94
@session_name ||= :default
-
end
-
-
##
-
#
-
# Yield a block using a specific session name.
-
#
-
1
def using_session(name)
-
previous_session_name = self.session_name
-
self.session_name = name
-
yield
-
ensure
-
self.session_name = previous_session_name
-
end
-
-
##
-
#
-
# Parse raw html into a document using Nokogiri, and adjust textarea contents as defined by the spec.
-
#
-
# @param [String] html The raw html
-
# @return [Nokogiri::HTML::Document] HTML document
-
#
-
1
def HTML(html)
-
Nokogiri::HTML(html).tap do |document|
-
document.xpath('//textarea').each do |textarea|
-
textarea['_capybara_raw_value'] = textarea.content.sub(/\A\n/,'')
-
end
-
end
-
end
-
-
# @deprecated Use default_max_wait_time instead
-
1
def default_wait_time
-
deprecate('default_wait_time', 'default_max_wait_time', true)
-
default_max_wait_time
-
end
-
-
# @deprecated Use default_max_wait_time= instead
-
1
def default_wait_time=(t)
-
deprecate('default_wait_time=', 'default_max_wait_time=')
-
self.default_max_wait_time = t
-
end
-
-
1
def save_and_open_page_path=(path)
-
warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
-
"Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?
-
@save_and_open_page_path = path
-
end
-
-
1
def app_host=(url)
-
raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
-
@app_host = url
-
end
-
-
1
def default_host=(url)
-
1
raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
-
1
@default_host = url
-
end
-
-
1
def included(base)
-
base.send(:include, Capybara::DSL)
-
warn "`include Capybara` is deprecated. Please use `include Capybara::DSL` instead."
-
end
-
-
1
def reuse_server=(bool)
-
1
warn "Capybara.reuse_server == false is a BETA feature and may change in a future version" unless bool
-
1
@reuse_server = bool
-
end
-
-
1
def deprecate(method, alternate_method, once=false)
-
@deprecation_notified ||= {}
-
warn "DEPRECATED: ##{method} is deprecated, please use ##{alternate_method} instead" unless once and @deprecation_notified[method]
-
@deprecation_notified[method]=true
-
end
-
-
1
private
-
-
1
def session_pool
-
108
@session_pool ||= {}
-
end
-
end
-
-
1
self.default_driver = nil
-
1
self.current_driver = nil
-
1
self.server_host = nil
-
-
1
module Driver; end
-
1
module RackTest; end
-
1
module Selenium; end
-
-
1
require 'capybara/helpers'
-
1
require 'capybara/session'
-
1
require 'capybara/window'
-
1
require 'capybara/server'
-
1
require 'capybara/selector'
-
1
require 'capybara/result'
-
1
require 'capybara/version'
-
-
1
require 'capybara/queries/base_query'
-
1
require 'capybara/queries/selector_query'
-
1
require 'capybara/queries/text_query'
-
1
require 'capybara/queries/title_query'
-
1
require 'capybara/queries/current_path_query'
-
1
require 'capybara/queries/match_query'
-
1
require 'capybara/query'
-
-
1
require 'capybara/node/finders'
-
1
require 'capybara/node/matchers'
-
1
require 'capybara/node/actions'
-
1
require 'capybara/node/document_matchers'
-
1
require 'capybara/node/simple'
-
1
require 'capybara/node/base'
-
1
require 'capybara/node/element'
-
1
require 'capybara/node/document'
-
-
1
require 'capybara/driver/base'
-
1
require 'capybara/driver/node'
-
-
1
require 'capybara/rack_test/driver'
-
1
require 'capybara/rack_test/node'
-
1
require 'capybara/rack_test/form'
-
1
require 'capybara/rack_test/browser'
-
1
require 'capybara/rack_test/css_handlers.rb'
-
-
1
require 'capybara/selenium/node'
-
1
require 'capybara/selenium/driver'
-
end
-
-
1
Capybara.register_server :default do |app, port, _host|
-
1
Capybara.run_default_server(app, port)
-
end
-
-
1
Capybara.register_server :webrick do |app, port, host|
-
1
require 'rack/handler/webrick'
-
1
Rack::Handler::WEBrick.run(app, Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0))
-
end
-
-
1
Capybara.register_server :puma do |app, port, host|
-
require 'rack/handler/puma'
-
Rack::Handler::Puma.run(app, Host: host, Port: port, Threads: "0:4")
-
end
-
-
1
Capybara.configure do |config|
-
1
config.always_include_port = false
-
1
config.run_server = true
-
1
config.server = :default
-
1
config.default_selector = :css
-
1
config.default_max_wait_time = 2
-
1
config.ignore_hidden_elements = true
-
1
config.default_host = "http://www.example.com"
-
1
config.automatic_reload = true
-
1
config.match = :smart
-
1
config.exact = false
-
1
config.exact_text = false
-
1
config.raise_server_errors = true
-
1
config.server_errors = [StandardError]
-
1
config.visible_text_only = false
-
1
config.wait_on_first_by_default = false
-
1
config.automatic_label_click = false
-
1
config.enable_aria_label = false
-
1
config.reuse_server = true
-
end
-
-
1
Capybara.register_driver :rack_test do |app|
-
Capybara::RackTest::Driver.new(app)
-
end
-
-
1
Capybara.register_driver :selenium do |app|
-
Capybara::Selenium::Driver.new(app)
-
end
-
-
# frozen_string_literal: true
-
1
class Capybara::Driver::Base
-
1
def current_url
-
raise NotImplementedError
-
end
-
-
1
def visit(path)
-
raise NotImplementedError
-
end
-
-
1
def find_xpath(query)
-
raise NotImplementedError
-
end
-
-
1
def find_css(query)
-
raise NotImplementedError
-
end
-
-
1
def html
-
raise NotImplementedError
-
end
-
-
1
def go_back
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#go_back'
-
end
-
-
1
def go_forward
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#go_forward'
-
end
-
-
1
def execute_script(script, *args)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#execute_script'
-
end
-
-
1
def evaluate_script(script, *args)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#evaluate_script'
-
end
-
-
1
def save_screenshot(path, options={})
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#save_screenshot'
-
end
-
-
1
def response_headers
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#response_headers'
-
end
-
-
1
def status_code
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#status_code'
-
end
-
-
##
-
#
-
# @param frame [Capybara::Node::Element, :parent, :top] The iframe element to switch to
-
#
-
1
def switch_to_frame(frame)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#switch_to_frame'
-
end
-
-
1
def current_window_handle
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#current_window_handle'
-
end
-
-
1
def window_size(handle)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#window_size'
-
end
-
-
1
def resize_window_to(handle, width, height)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#resize_window_to'
-
end
-
-
1
def maximize_window(handle)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#maximize_current_window'
-
end
-
-
1
def close_window(handle)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#close_window'
-
end
-
-
1
def window_handles
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#window_handles'
-
end
-
-
1
def open_new_window
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#open_new_window'
-
end
-
-
1
def switch_to_window(handle)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#switch_to_window'
-
end
-
-
1
def within_window(locator)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#within_window'
-
end
-
-
1
def no_such_window_error
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#no_such_window_error'
-
end
-
-
-
##
-
#
-
# Execute the block, and then accept the modal opened.
-
# @param type [:alert, :confirm, :prompt]
-
# @option options [Numeric] :wait How long to wait for the modal to appear after executing the block.
-
# @option options [String, Regexp] :text Text to verify is in the message shown in the modal
-
# @option options [String] :with Text to fill in in the case of a prompt
-
# @return [String] the message shown in the modal
-
# @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
-
#
-
1
def accept_modal(type, options={}, &blk)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#accept_modal'
-
end
-
-
##
-
#
-
# Execute the block, and then dismiss the modal opened.
-
# @param type [:alert, :confirm, :prompt]
-
# @option options [Numeric] :wait How long to wait for the modal to appear after executing the block.
-
# @option options [String, Regexp] :text Text to verify is in the message shown in the modal
-
# @return [String] the message shown in the modal
-
# @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
-
#
-
1
def dismiss_modal(type, options={}, &blk)
-
raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#dismiss_modal'
-
end
-
-
1
def invalid_element_errors
-
[]
-
end
-
-
1
def wait?
-
false
-
end
-
-
1
def reset!
-
end
-
-
1
def needs_server?
-
false
-
end
-
-
# @deprecated This method is being removed
-
1
def browser_initialized?
-
warn "DEPRECATED: #browser_initialized? is deprecated and will be removed in the next version of Capybara"
-
true
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Driver
-
1
class Node
-
1
attr_reader :driver, :native
-
-
1
def initialize(driver, native)
-
78
@driver = driver
-
78
@native = native
-
end
-
-
1
def all_text
-
raise NotImplementedError
-
end
-
-
1
def visible_text
-
raise NotImplementedError
-
end
-
-
1
def [](name)
-
raise NotImplementedError
-
end
-
-
1
def value
-
raise NotImplementedError
-
end
-
-
# @param value String or Array. Array is only allowed if node has 'multiple' attribute
-
# @param options [Hash{}] Driver specific options for how to set a value on a node
-
1
def set(value, options={})
-
raise NotImplementedError
-
end
-
-
1
def select_option
-
raise NotImplementedError
-
end
-
-
1
def unselect_option
-
raise NotImplementedError
-
end
-
-
1
def click
-
raise NotImplementedError
-
end
-
-
1
def right_click
-
raise NotImplementedError
-
end
-
-
1
def double_click
-
raise NotImplementedError
-
end
-
-
1
def send_keys(*args)
-
raise NotImplementedError
-
end
-
-
1
def hover
-
raise NotImplementedError
-
end
-
-
1
def drag_to(element)
-
raise NotImplementedError
-
end
-
-
1
def tag_name
-
raise NotImplementedError
-
end
-
-
1
def visible?
-
raise NotImplementedError
-
end
-
-
1
def checked?
-
raise NotImplementedError
-
end
-
-
1
def selected?
-
raise NotImplementedError
-
end
-
-
1
def disabled?
-
raise NotImplementedError
-
end
-
-
1
def readonly?
-
!!self[:readonly]
-
end
-
-
1
def multiple?
-
!!self[:multiple]
-
end
-
-
1
def path
-
raise NotSupportedByDriverError, 'Capybara::Driver::Node#path'
-
end
-
-
1
def trigger(event)
-
raise NotSupportedByDriverError, 'Capybara::Driver::Node#trigger'
-
end
-
-
1
def inspect
-
%(#<#{self.class} tag="#{tag_name}" path="#{path}">)
-
rescue NotSupportedByDriverError
-
%(#<#{self.class} tag="#{tag_name}">)
-
end
-
-
1
def ==(other)
-
raise NotSupportedByDriverError, 'Capybara::Driver::Node#=='
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara'
-
-
1
module Capybara
-
1
module DSL
-
1
def self.included(base)
-
5
warn "including Capybara::DSL in the global scope is not recommended!" if base == Object
-
5
super
-
end
-
-
1
def self.extended(base)
-
1
warn "extending the main object with Capybara::DSL is not recommended!" if base == TOPLEVEL_BINDING.eval("self")
-
1
super
-
end
-
-
##
-
#
-
# Shortcut to working in a different session.
-
#
-
1
def using_session(name, &block)
-
Capybara.using_session(name, &block)
-
end
-
-
##
-
#
-
# Shortcut to using a different wait time.
-
#
-
1
def using_wait_time(seconds, &block)
-
Capybara.using_wait_time(seconds, &block)
-
end
-
-
##
-
#
-
# Shortcut to accessing the current session.
-
#
-
# class MyClass
-
# include Capybara::DSL
-
#
-
# def has_header?
-
# page.has_css?('h1')
-
# end
-
# end
-
#
-
# @return [Capybara::Session] The current session object
-
#
-
1
def page
-
94
Capybara.current_session
-
end
-
-
1
Session::DSL_METHODS.each do |method|
-
99
define_method method do |*args, &block|
-
91
page.send method, *args, &block
-
end
-
end
-
end
-
-
1
extend(Capybara::DSL)
-
end
-
# encoding: UTF-8
-
# frozen_string_literal: true
-
-
1
module Capybara
-
-
# @api private
-
1
module Helpers
-
1
extend self
-
-
##
-
#
-
# Normalizes whitespace space by stripping leading and trailing
-
# whitespace and replacing sequences of whitespace characters
-
# with a single space.
-
#
-
# @param [String] text Text to normalize
-
# @return [String] Normalized text
-
#
-
1
def normalize_whitespace(text)
-
8
text.to_s.gsub(/[[:space:]]+/, ' ').strip
-
end
-
-
##
-
#
-
# Escapes any characters that would have special meaning in a regexp
-
# if text is not a regexp
-
#
-
# @param [String] text Text to escape
-
# @return [String] Escaped text
-
#
-
1
def to_regexp(text, regexp_options=nil, exact=false)
-
2
if text.is_a?(Regexp)
-
text
-
else
-
2
escaped = Regexp.escape(normalize_whitespace(text))
-
2
escaped = "\\A#{escaped}\\z" if exact
-
2
Regexp.new(escaped, regexp_options)
-
end
-
end
-
-
##
-
#
-
# Injects a `<base>` tag into the given HTML code, pointing to
-
# `Capybara.asset_host`.
-
#
-
# @param [String] html HTML code to inject into
-
# @return [String] The modified HTML code
-
#
-
1
def inject_asset_host(html)
-
if Capybara.asset_host && Nokogiri::HTML(html).css("base").empty?
-
match = html.match(/<head[^<]*?>/)
-
if match
-
return html.clone.insert match.end(0), "<base href='#{Capybara.asset_host}' />"
-
end
-
end
-
-
html
-
end
-
-
##
-
#
-
# A poor man's `pluralize`. Given two declensions, one singular and one
-
# plural, as well as a count, this will pick the correct declension. This
-
# way we can generate grammatically correct error message.
-
#
-
# @param [String] singular The singular form of the word
-
# @param [String] plural The plural form of the word
-
# @param [Integer] count The number of items
-
#
-
1
def declension(singular, plural, count)
-
if count == 1
-
singular
-
else
-
plural
-
end
-
end
-
-
1
if defined?(Process::CLOCK_MONOTONIC)
-
1
def monotonic_time
-
392
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def monotonic_time
-
Time.now.to_f
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
1
module Actions
-
-
##
-
#
-
# Finds a button or link by id, text or value and clicks it. Also looks at image
-
# alt text inside the link.
-
# @!macro waiting_behavior
-
# If the driver is capable of executing JavaScript, +$0+ will wait for a set amount of time
-
# and continuously retry finding the element until either the element is found or the time
-
# expires. The length of time +find+ will wait is controlled through {Capybara.default_max_wait_time}
-
#
-
# @option options [false, Numeric] wait (Capybara.default_max_wait_time) Maximum time to wait for matching element to appear.
-
#
-
# @overload click_link_or_button([locator], options)
-
#
-
# @param [String] locator Text, id or value of link or button
-
#
-
# @return [Capybara::Node::Element] The element clicked
-
#
-
1
def click_link_or_button(locator=nil, options={})
-
locator, options = nil, locator if locator.is_a? Hash
-
find(:link_or_button, locator, options).click
-
end
-
1
alias_method :click_on, :click_link_or_button
-
-
##
-
#
-
# Finds a link by id, text or title and clicks it. Also looks at image
-
# alt text inside the link.
-
#
-
# @macro waiting_behavior
-
#
-
# @overload click_link([locator], options)
-
# @param [String] locator text, id, title or nested image's alt attribute
-
# @param options See {Capybara::Node::Finders#find_link}
-
#
-
# @return [Capybara::Node::Element] The element clicked
-
1
def click_link(locator=nil, options={})
-
locator, options = nil, locator if locator.is_a? Hash
-
find(:link, locator, options).click
-
end
-
-
##
-
#
-
# Finds a button on the page and clicks it.
-
# This can be any \<input> element of type submit, reset, image, button or it can be a
-
# \<button> element. All buttons can be found by their id, value, or title. \<button> elements can also be found
-
# by their text content, and image \<input> elements by their alt attribute
-
#
-
# @macro waiting_behavior
-
#
-
# @overload click_button([locator], options)
-
# @param [String] locator Which button to find
-
# @param options See {Capybara::Node::Finders#find_button}
-
# @return [Capybara::Node::Element] The element clicked
-
1
def click_button(locator=nil, options={})
-
15
locator, options = nil, locator if locator.is_a? Hash
-
15
find(:button, locator, options).click
-
end
-
-
##
-
#
-
# Locate a text field or text area and fill it in with the given text
-
# The field can be found via its name, id or label text.
-
#
-
# page.fill_in 'Name', with: 'Bob'
-
#
-
#
-
# @overload fill_in([locator], options={})
-
# @param [String] locator Which field to fill in
-
# @param [Hash] options
-
# @macro waiting_behavior
-
# @option options [String] :with The value to fill in - required
-
# @option options [Hash] :fill_options Driver specific options regarding how to fill fields
-
# @option options [String] :currently_with The current value property of the field to fill in
-
# @option options [Boolean] :multiple Match fields that can have multiple values?
-
# @option options [String] :id Match fields that match the id attribute
-
# @option options [String] :name Match fields that match the name attribute
-
# @option options [String] :placeholder Match fields that match the placeholder attribute
-
# @option options [String, Array<String>] :class Match links that match the class(es) provided
-
#
-
# @return [Capybara::Node::Element] The element filled_in
-
1
def fill_in(locator, options={})
-
61
locator, options = nil, locator if locator.is_a? Hash
-
61
raise "Must pass a hash containing 'with'" if not options.is_a?(Hash) or not options.has_key?(:with)
-
61
with = options.delete(:with)
-
61
fill_options = options.delete(:fill_options)
-
61
options[:with] = options.delete(:currently_with) if options.has_key?(:currently_with)
-
61
find(:fillable_field, locator, options).set(with, fill_options)
-
end
-
-
# @!macro label_click
-
# @option options [Boolean] :allow_label_click (Capybara.automatic_label_click) Attempt to click the label to toggle state if element is non-visible.
-
-
##
-
#
-
# Find a radio button and mark it as checked. The radio button can be found
-
# via name, id or label text.
-
#
-
# page.choose('Male')
-
#
-
# @overload choose([locator], options)
-
# @param [String] locator Which radio button to choose
-
#
-
# @option options [String] :option Value of the radio_button to choose
-
# @option options [String] :id Match fields that match the id attribute
-
# @option options [String] :name Match fields that match the name attribute
-
# @option options [String, Array<String>] :class Match links that match the class(es) provided
-
# @macro waiting_behavior
-
# @macro label_click
-
#
-
# @return [Capybara::Node::Element] The element chosen or the label clicked
-
1
def choose(locator, options={})
-
_check_with_label(:radio_button, true, locator, options)
-
end
-
-
##
-
#
-
# Find a check box and mark it as checked. The check box can be found
-
# via name, id or label text.
-
#
-
# page.check('German')
-
#
-
#
-
# @overload check([locator], options)
-
# @param [String] locator Which check box to check
-
#
-
# @option options [String] :option Value of the checkbox to select
-
# @option options [String] id Match fields that match the id attribute
-
# @option options [String] name Match fields that match the name attribute
-
# @option options [String, Array<String>] :class Match links that match the class(es) provided
-
# @macro label_click
-
# @macro waiting_behavior
-
#
-
# @return [Capybara::Node::Element] The element checked or the label clicked
-
1
def check(locator, options={})
-
_check_with_label(:checkbox, true, locator, options)
-
end
-
-
##
-
#
-
# Find a check box and mark uncheck it. The check box can be found
-
# via name, id or label text.
-
#
-
# page.uncheck('German')
-
#
-
#
-
# @overload uncheck([locator], options)
-
# @param [String] locator Which check box to uncheck
-
#
-
# @option options [String] :option Value of the checkbox to deselect
-
# @option options [String] id Match fields that match the id attribute
-
# @option options [String] name Match fields that match the name attribute
-
# @option options [String, Array<String>] :class Match links that match the class(es) provided
-
# @macro label_click
-
# @macro waiting_behavior
-
#
-
# @return [Capybara::Node::Element] The element unchecked or the label clicked
-
1
def uncheck(locator, options={})
-
_check_with_label(:checkbox, false, locator, options)
-
end
-
-
##
-
#
-
# If `:from` option is present, `select` finds a select box on the page
-
# and selects a particular option from it.
-
# Otherwise it finds an option inside current scope and selects it.
-
# If the select box is a multiple select, +select+ can be called multiple times to select more than
-
# one option.
-
# The select box can be found via its name, id or label text. The option can be found by its text.
-
#
-
# page.select 'March', from: 'Month'
-
#
-
# @macro waiting_behavior
-
#
-
# @param [String] value Which option to select
-
# @option options [String] :from The id, name or label of the select box
-
#
-
# @return [Capybara::Node::Element] The option element selected
-
1
def select(value, options={})
-
if options.has_key?(:from)
-
from = options.delete(:from)
-
find(:select, from, options).find(:option, value, options).select_option
-
else
-
find(:option, value, options).select_option
-
end
-
end
-
-
##
-
#
-
# Find a select box on the page and unselect a particular option from it. If the select
-
# box is a multiple select, +unselect+ can be called multiple times to unselect more than
-
# one option. The select box can be found via its name, id or label text.
-
#
-
# page.unselect 'March', from: 'Month'
-
#
-
# @macro waiting_behavior
-
#
-
# @param [String] value Which option to unselect
-
# @param [Hash{:from => String}] options The id, name or label of the select box
-
#
-
# @return [Capybara::Node::Element] The option element unselected
-
1
def unselect(value, options={})
-
if options.has_key?(:from)
-
from = options.delete(:from)
-
find(:select, from, options).find(:option, value, options).unselect_option
-
else
-
find(:option, value, options).unselect_option
-
end
-
end
-
-
##
-
#
-
# Find a file field on the page and attach a file given its path. The file field can
-
# be found via its name, id or label text.
-
#
-
# page.attach_file(locator, '/path/to/file.png')
-
#
-
# @macro waiting_behavior
-
#
-
# @param [String] locator Which field to attach the file to
-
# @param [String] path The path of the file that will be attached, or an array of paths
-
#
-
# @option options [Symbol] match (Capybara.match) The matching strategy to use (:one, :first, :prefer_exact, :smart).
-
# @option options [Boolean] exact (Capybara.exact) Match the exact label name/contents or accept a partial match.
-
# @option options [Boolean] multiple Match field which allows multiple file selection
-
# @option options [String] id Match fields that match the id attribute
-
# @option options [String] name Match fields that match the name attribute
-
# @option options [String, Array<String>] :class Match links that match the class(es) provided
-
# @option options [true, Hash] make_visible A Hash of CSS styles to change before attempting to attach the file, if `true` { opacity: 1, display: 'block', visibility: 'visible' } is used (may not be supported by all drivers)
-
#
-
# @return [Capybara::Node::Element] The file field element
-
1
def attach_file(locator, path, options={})
-
locator, path, options = nil, locator, path if path.is_a? Hash
-
Array(path).each do |p|
-
raise Capybara::FileNotFound, "cannot attach file, #{p} does not exist" unless File.exist?(p.to_s)
-
end
-
# Allow user to update the CSS style of the file input since they are so often hidden on a page
-
if style = options.delete(:make_visible)
-
style = { opacity: 1, display: 'block', visibility: 'visible' } if style == true
-
ff = find(:file_field, locator, options.merge({visible: :all}))
-
_update_style(ff, style)
-
if ff.visible?
-
begin
-
ff.set(path)
-
ensure
-
_reset_style(ff)
-
end
-
else
-
raise ExpectationNotMet, "The style changes in :make_visible did not make the file input visible"
-
end
-
else
-
find(:file_field, locator, options).set(path)
-
end
-
end
-
-
1
private
-
1
def _update_style(element, style)
-
script = <<-JS
-
var el = arguments[0];
-
el.capybara_style_cache = el.style.cssText;
-
var css = arguments[1];
-
for (var prop in css){
-
if (css.hasOwnProperty(prop)) {
-
el.style[prop] = css[prop]
-
}
-
}
-
JS
-
begin
-
session.execute_script(script, element, style)
-
rescue Capybara::NotSupportedByDriverError
-
warn "The :make_visible option is not supported by the current driver - ignoring"
-
end
-
end
-
-
1
def _reset_style(element)
-
script = <<-JS
-
var el = arguments[0];
-
if (el.hasOwnProperty('capybara_style_cache')) {
-
el.style.cssText = el.capybara_style_cache;
-
delete el.capybara_style_cache;
-
}
-
JS
-
begin
-
session.execute_script(script, element)
-
rescue
-
end
-
end
-
-
-
1
def _check_with_label(selector, checked, locator, options)
-
locator, options = nil, locator if locator.is_a? Hash
-
allow_label_click = options.delete(:allow_label_click) { Capybara.automatic_label_click }
-
-
synchronize(Capybara::Queries::BaseQuery::wait(options)) do
-
begin
-
el = find(selector, locator, options)
-
el.set(checked)
-
rescue => e
-
raise unless allow_label_click && catch_error?(e)
-
begin
-
el ||= find(selector, locator, options.merge(visible: :all))
-
label = find(:label, for: el, visible: true)
-
label.click unless (el.checked? == checked)
-
rescue
-
raise e
-
end
-
end
-
end
-
end
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
-
##
-
#
-
# A {Capybara::Node::Base} represents either an element on a page through the subclass
-
# {Capybara::Node::Element} or a document through {Capybara::Node::Document}.
-
#
-
# Both types of Node share the same methods, used for interacting with the
-
# elements on the page. These methods are divided into three categories,
-
# finders, actions and matchers. These are found in the modules
-
# {Capybara::Node::Finders}, {Capybara::Node::Actions} and {Capybara::Node::Matchers}
-
# respectively.
-
#
-
# A {Capybara::Session} exposes all methods from {Capybara::Node::Document} directly:
-
#
-
# session = Capybara::Session.new(:rack_test, my_app)
-
# session.visit('/')
-
# session.fill_in('Foo', with: 'Bar') # from Capybara::Node::Actions
-
# bar = session.find('#bar') # from Capybara::Node::Finders
-
# bar.select('Baz', from: 'Quox') # from Capybara::Node::Actions
-
# session.has_css?('#foobar') # from Capybara::Node::Matchers
-
#
-
1
class Base
-
1
attr_reader :session, :base, :query_scope
-
-
1
include Capybara::Node::Finders
-
1
include Capybara::Node::Actions
-
1
include Capybara::Node::Matchers
-
-
1
def initialize(session, base)
-
79
@session = session
-
79
@base = base
-
end
-
-
# overridden in subclasses, e.g. Capybara::Node::Element
-
1
def reload
-
self
-
end
-
-
##
-
#
-
# This method is Capybara's primary defence against asynchronicity
-
# problems. It works by attempting to run a given block of code until it
-
# succeeds. The exact behaviour of this method depends on a number of
-
# factors. Basically there are certain exceptions which, when raised
-
# from the block, instead of bubbling up, are caught, and the block is
-
# re-run.
-
#
-
# Certain drivers, such as RackTest, have no support for asynchronous
-
# processes, these drivers run the block, and any error raised bubbles up
-
# immediately. This allows faster turn around in the case where an
-
# expectation fails.
-
#
-
# Only exceptions that are {Capybara::ElementNotFound} or any subclass
-
# thereof cause the block to be rerun. Drivers may specify additional
-
# exceptions which also cause reruns. This usually occurs when a node is
-
# manipulated which no longer exists on the page. For example, the
-
# Selenium driver specifies
-
# `Selenium::WebDriver::Error::ObsoleteElementError`.
-
#
-
# As long as any of these exceptions are thrown, the block is re-run,
-
# until a certain amount of time passes. The amount of time defaults to
-
# {Capybara.default_max_wait_time} and can be overridden through the `seconds`
-
# argument. This time is compared with the system time to see how much
-
# time has passed. On rubies/platforms which don't support access to a monotonic process clock
-
# if the return value of `Time.now` is stubbed out, Capybara will raise `Capybara::FrozenInTime`.
-
#
-
# @param [Integer] seconds Number of seconds to retry this block
-
# @param options [Hash]
-
# @option options [Array<Exception>] :errors (driver.invalid_element_errors +
-
# [Capybara::ElementNotFound]) exception types that cause the block to be rerun
-
# @return [Object] The result of the given block
-
# @raise [Capybara::FrozenInTime] If the return value of `Time.now` appears stuck
-
#
-
1
def synchronize(seconds=Capybara.default_max_wait_time, options = {})
-
392
start_time = Capybara::Helpers.monotonic_time
-
-
392
if session.synchronized
-
237
yield
-
else
-
155
session.synchronized = true
-
155
begin
-
155
yield
-
rescue => e
-
session.raise_server_error!
-
raise e unless driver.wait?
-
raise e unless catch_error?(e, options[:errors])
-
raise e if (Capybara::Helpers.monotonic_time - start_time) >= seconds
-
sleep(0.05)
-
raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Capybara::Helpers.monotonic_time == start_time
-
reload if Capybara.automatic_reload
-
retry
-
ensure
-
155
session.synchronized = false
-
155
end
-
end
-
end
-
-
# @api private
-
1
def find_css(css)
-
base.find_css(css)
-
end
-
-
# @api private
-
1
def find_xpath(xpath)
-
79
base.find_xpath(xpath)
-
end
-
-
# @deprecated Use query_scope instead
-
1
def parent
-
warn "DEPRECATED: #parent is deprecated in favor of #query_scope - Note: #parent was not the elements parent in the document so it's most likely not what you wanted anyway"
-
query_scope
-
end
-
-
1
protected
-
-
1
def catch_error?(error, errors = nil)
-
errors ||= (driver.invalid_element_errors + [Capybara::ElementNotFound])
-
errors.any? do |type|
-
error.is_a?(type)
-
end
-
end
-
-
1
def driver
-
session.driver
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
-
##
-
#
-
# A {Capybara::Document} represents an HTML document. Any operation
-
# performed on it will be performed on the entire document.
-
#
-
# @see Capybara::Node
-
#
-
1
class Document < Base
-
1
include Capybara::Node::DocumentMatchers
-
-
1
def inspect
-
%(#<Capybara::Document>)
-
end
-
-
##
-
#
-
# @return [String] The text of the document
-
#
-
1
def text(type=nil)
-
2
find(:xpath, '/html').text(type)
-
end
-
-
##
-
#
-
# @return [String] The title of the document
-
#
-
1
def title
-
session.driver.title
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
1
module DocumentMatchers
-
##
-
# Asserts that the page has the given title.
-
#
-
# @!macro title_query_params
-
# @overload $0(string, options = {})
-
# @param string [String] The string that title should include
-
# @overload $0(regexp, options = {})
-
# @param regexp [Regexp] The regexp that title should match to
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for title to eq/match given string/regexp argument
-
# @option options [Boolean] :exact (false) When passed a string should the match be exact or just substring
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_title(title, options = {})
-
_verify_title(title,options) { |query| raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self) }
-
end
-
-
##
-
# Asserts that the page doesn't have the given title.
-
#
-
# @macro title_query_params
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_no_title(title, options = {})
-
_verify_title(title,options) { |query| raise Capybara::ExpectationNotMet, query.negative_failure_message if query.resolves_for?(self) }
-
end
-
-
##
-
# Checks if the page has the given title.
-
#
-
# @macro title_query_params
-
# @return [Boolean]
-
#
-
1
def has_title?(title, options = {})
-
assert_title(title, options)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
# Checks if the page doesn't have the given title.
-
#
-
# @macro title_query_params
-
# @return [Boolean]
-
#
-
1
def has_no_title?(title, options = {})
-
assert_no_title(title, options)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
1
private
-
-
1
def _verify_title(title, options)
-
query = Capybara::Queries::TitleQuery.new(title, options)
-
synchronize(query.wait) do
-
yield(query)
-
end
-
return true
-
end
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
-
##
-
#
-
# A {Capybara::Node::Element} represents a single element on the page. It is possible
-
# to interact with the contents of this element the same as with a document:
-
#
-
# session = Capybara::Session.new(:rack_test, my_app)
-
#
-
# bar = session.find('#bar') # from Capybara::Node::Finders
-
# bar.select('Baz', from: 'Quox') # from Capybara::Node::Actions
-
#
-
# {Capybara::Node::Element} also has access to HTML attributes and other properties of the
-
# element:
-
#
-
# bar.value
-
# bar.text
-
# bar[:title]
-
#
-
# @see Capybara::Node
-
#
-
1
class Element < Base
-
-
1
def initialize(session, base, query_scope, query)
-
78
super(session, base)
-
78
@query_scope = query_scope
-
78
@query = query
-
78
@allow_reload = false
-
end
-
-
1
def allow_reload!
-
78
@allow_reload = true
-
end
-
-
##
-
#
-
# @return [Object] The native element from the driver, this allows access to driver specific methods
-
#
-
1
def native
-
synchronize { base.native }
-
end
-
-
##
-
#
-
# Retrieve the text of the element. If `Capybara.ignore_hidden_elements`
-
# is `true`, which it is by default, then this will return only text
-
# which is visible. The exact semantics of this may differ between
-
# drivers, but generally any text within elements with `display:none` is
-
# ignored. This behaviour can be overridden by passing `:all` to this
-
# method.
-
#
-
# @param [:all, :visible] type Whether to return only visible or all text
-
# @return [String] The text of the element
-
#
-
1
def text(type=nil)
-
2
type ||= :all unless Capybara.ignore_hidden_elements or Capybara.visible_text_only
-
2
synchronize do
-
2
if type == :all
-
base.all_text
-
else
-
2
base.visible_text
-
end
-
end
-
end
-
-
##
-
#
-
# Retrieve the given attribute
-
#
-
# element[:title] # => HTML title attribute
-
#
-
# @param [Symbol] attribute The attribute to retrieve
-
# @return [String] The value of the attribute
-
#
-
1
def [](attribute)
-
synchronize { base[attribute] }
-
end
-
-
##
-
#
-
# @return [String] The value of the form element
-
#
-
1
def value
-
synchronize { base.value }
-
end
-
-
##
-
#
-
# Set the value of the form element to the given value.
-
#
-
# @param [String] value The new value
-
# @param [Hash{}] options Driver specific options for how to set the value
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def set(value, options={})
-
61
options ||= {}
-
-
61
driver_supports_options = (base.method(:set).arity != 1)
-
-
61
unless options.empty? || driver_supports_options
-
warn "Options passed to Capybara::Node#set but the driver doesn't support them"
-
end
-
-
61
synchronize do
-
61
if driver_supports_options
-
base.set(value, options)
-
else
-
61
base.set(value)
-
end
-
end
-
61
return self
-
end
-
-
##
-
#
-
# Select this node if is an option element inside a select tag
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def select_option
-
warn "Attempt to select disabled option: #{value || text}" if disabled?
-
synchronize { base.select_option }
-
return self
-
end
-
-
##
-
#
-
# Unselect this node if is an option element inside a multiple select tag
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def unselect_option
-
synchronize { base.unselect_option }
-
return self
-
end
-
-
##
-
#
-
# Click the Element
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def click
-
30
synchronize { base.click }
-
15
return self
-
end
-
-
##
-
#
-
# Right Click the Element
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def right_click
-
synchronize { base.right_click }
-
return self
-
end
-
-
##
-
#
-
# Double Click the Element
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def double_click
-
synchronize { base.double_click }
-
return self
-
end
-
-
##
-
#
-
# Send Keystrokes to the Element
-
#
-
# @overload send_keys(keys, ...)
-
# @param [String, Symbol, Array<String,Symbol>] keys
-
#
-
# Examples:
-
#
-
# element.send_keys "foo" #=> value: 'foo'
-
# element.send_keys "tet", :left, "s" #=> value: 'test'
-
# element.send_keys [:control, 'a'], :space #=> value: ' ' - assuming ctrl-a selects all contents
-
#
-
# Symbols supported for keys
-
# :cancel
-
# :help
-
# :backspace
-
# :tab
-
# :clear
-
# :return
-
# :enter
-
# :shift
-
# :control
-
# :alt
-
# :pause
-
# :escape
-
# :space
-
# :page_up
-
# :page_down
-
# :end
-
# :home
-
# :left
-
# :up
-
# :right
-
# :down
-
# :insert
-
# :delete
-
# :semicolon
-
# :equals
-
# :numpad0
-
# :numpad1
-
# :numpad2
-
# :numpad3
-
# :numpad4
-
# :numpad5
-
# :numpad6
-
# :numpad7
-
# :numpad8
-
# :numpad9
-
# :multiply - numeric keypad *
-
# :add - numeric keypad +
-
# :separator - numeric keypad 'separator' key ??
-
# :subtract - numeric keypad -
-
# :decimal - numeric keypad .
-
# :divide - numeric keypad /
-
# :f1
-
# :f2
-
# :f3
-
# :f4
-
# :f5
-
# :f6
-
# :f7
-
# :f8
-
# :f9
-
# :f10
-
# :f11
-
# :f12
-
# :meta
-
# :command - alias of :meta
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def send_keys(*args)
-
synchronize { base.send_keys(*args) }
-
return self
-
end
-
-
##
-
#
-
# Hover on the Element
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def hover
-
synchronize { base.hover }
-
return self
-
end
-
-
##
-
#
-
# @return [String] The tag name of the element
-
#
-
1
def tag_name
-
synchronize { base.tag_name }
-
end
-
-
##
-
#
-
# Whether or not the element is visible. Not all drivers support CSS, so
-
# the result may be inaccurate.
-
#
-
# @return [Boolean] Whether the element is visible
-
#
-
1
def visible?
-
156
synchronize { base.visible? }
-
end
-
-
##
-
#
-
# Whether or not the element is checked.
-
#
-
# @return [Boolean] Whether the element is checked
-
#
-
1
def checked?
-
synchronize { base.checked? }
-
end
-
-
##
-
#
-
# Whether or not the element is selected.
-
#
-
# @return [Boolean] Whether the element is selected
-
#
-
1
def selected?
-
synchronize { base.selected? }
-
end
-
-
##
-
#
-
# Whether or not the element is disabled.
-
#
-
# @return [Boolean] Whether the element is disabled
-
#
-
1
def disabled?
-
152
synchronize { base.disabled? }
-
end
-
-
##
-
#
-
# Whether or not the element is readonly.
-
#
-
# @return [Boolean] Whether the element is readonly
-
#
-
1
def readonly?
-
synchronize { base.readonly? }
-
end
-
-
##
-
#
-
# Whether or not the element supports multiple results.
-
#
-
# @return [Boolean] Whether the element supports multiple results.
-
#
-
1
def multiple?
-
synchronize { base.multiple? }
-
end
-
-
##
-
#
-
# An XPath expression describing where on the page the element can be found
-
#
-
# @return [String] An XPath expression
-
#
-
1
def path
-
synchronize { base.path }
-
end
-
-
##
-
#
-
# Trigger any event on the current element, for example mouseover or focus
-
# events. Does not work in Selenium.
-
#
-
# @param [String] event The name of the event to trigger
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def trigger(event)
-
synchronize { base.trigger(event) }
-
return self
-
end
-
-
##
-
#
-
# Drag the element to the given other element.
-
#
-
# source = page.find('#foo')
-
# target = page.find('#bar')
-
# source.drag_to(target)
-
#
-
# @param [Capybara::Node::Element] node The element to drag to
-
#
-
# @return [Capybara::Node::Element] The element
-
1
def drag_to(node)
-
synchronize { base.drag_to(node.base) }
-
return self
-
end
-
-
1
def reload
-
if @allow_reload
-
begin
-
reloaded = query_scope.reload.first(@query.name, @query.locator, @query.options)
-
@base = reloaded.base if reloaded
-
rescue => e
-
raise e unless catch_error?(e)
-
end
-
end
-
self
-
end
-
-
1
def inspect
-
%(#<Capybara::Node::Element tag="#{tag_name}" path="#{path}">)
-
rescue NotSupportedByDriverError
-
%(#<Capybara::Node::Element tag="#{tag_name}">)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
1
module Finders
-
-
##
-
#
-
# Find an {Capybara::Node::Element} based on the given arguments. +find+ will raise an error if the element
-
# is not found.
-
#
-
# @!macro waiting_behavior
-
# If the driver is capable of executing JavaScript, +$0+ will wait for a set amount of time
-
# and continuously retry finding the element until either the element is found or the time
-
# expires. The length of time +find+ will wait is controlled through {Capybara.default_max_wait_time}
-
# and defaults to 2 seconds.
-
# @option options [false, Numeric] wait (Capybara.default_max_wait_time) Maximum time to wait for matching element to appear.
-
#
-
# +find+ takes the same options as +all+.
-
#
-
# page.find('#foo').find('.bar')
-
# page.find(:xpath, './/div[contains(., "bar")]')
-
# page.find('li', text: 'Quox').click_link('Delete')
-
#
-
# @param (see Capybara::Node::Finders#all)
-
#
-
# @option options [Boolean] match The matching strategy to use.
-
#
-
# @return [Capybara::Node::Element] The found element
-
# @raise [Capybara::ElementNotFound] If the element can't be found before time expires
-
#
-
1
def find(*args, &optional_filter_block)
-
78
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
-
synchronize(query.wait) do
-
78
if (query.match == :smart or query.match == :prefer_exact) and query.supports_exact?
-
76
result = query.resolve_for(self, true)
-
76
result = query.resolve_for(self, false) if result.empty? && !query.exact?
-
else
-
2
result = query.resolve_for(self)
-
end
-
78
if query.match == :one or query.match == :smart and result.size > 1
-
raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
-
end
-
78
if result.empty?
-
raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
-
end
-
78
result.first
-
78
end.tap(&:allow_reload!)
-
end
-
-
##
-
#
-
# Find a form field on the page. The field can be found by its name, id or label text.
-
#
-
# @overload find_field([locator], options={})
-
# @param [String] locator name, id, placeholder or text of associated label element
-
#
-
# @macro waiting_behavior
-
#
-
#
-
# @option options [Boolean] checked Match checked field?
-
# @option options [Boolean] unchecked Match unchecked field?
-
# @option options [Boolean, Symbol] disabled (false) Match disabled field?
-
# * true - only finds a disabled field
-
# * false - only finds an enabled field
-
# * :all - finds either an enabled or disabled field
-
# @option options [Boolean] readonly Match readonly field?
-
# @option options [String, Regexp] with Value of field to match on
-
# @option options [String] type Type of field to match on
-
# @option options [Boolean] multiple Match fields that can have multiple values?
-
# @option options [String] id Match fields that match the id attribute
-
# @option options [String] name Match fields that match the name attribute
-
# @option options [String] placeholder Match fields that match the placeholder attribute
-
# @option options [String, Array<String>] Match fields that match the class(es) passed
-
# @return [Capybara::Node::Element] The found element
-
#
-
-
1
def find_field(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
find(:field, locator, options, &optional_filter_block)
-
end
-
1
alias_method :field_labeled, :find_field
-
-
##
-
#
-
# Find a link on the page. The link can be found by its id or text.
-
#
-
# @overload find_link([locator], options={})
-
# @param [String] locator id, title, text, or alt of enclosed img element
-
#
-
# @macro waiting_behavior
-
#
-
# @option options [String,Regexp,nil] href Value to match against the links href, if nil finds link placeholders (<a> elements with no href attribute)
-
# @option options [String] id Match links with the id provided
-
# @option options [String] title Match links with the title provided
-
# @option options [String] alt Match links with a contained img element whose alt matches
-
# @option options [String, Array<String>] class Match links that match the class(es) provided
-
# @return [Capybara::Node::Element] The found element
-
#
-
1
def find_link(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
find(:link, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Find a button on the page.
-
# This can be any \<input> element of type submit, reset, image, button or it can be a
-
# \<button> element. All buttons can be found by their id, value, or title. \<button> elements can also be found
-
# by their text content, and image \<input> elements by their alt attribute
-
#
-
# @overload find_button([locator], options={})
-
# @param [String] locator id, value, title, text content, alt of image
-
#
-
# @overload find_button(options={})
-
#
-
# @macro waiting_behavior
-
#
-
# @option options [Boolean, Symbol] disabled (false) Match disabled button?
-
# * true - only finds a disabled button
-
# * false - only finds an enabled button
-
# * :all - finds either an enabled or disabled button
-
# @option options [String] id Match buttons with the id provided
-
# @option options [String] title Match buttons with the title provided
-
# @option options [String] value Match buttons with the value provided
-
# @option options [String, Array<String>] class Match links that match the class(es) provided
-
# @return [Capybara::Node::Element] The found element
-
#
-
1
def find_button(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
find(:button, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Find a element on the page, given its id.
-
#
-
# @macro waiting_behavior
-
#
-
# @param [String] id id of element
-
#
-
# @return [Capybara::Node::Element] The found element
-
#
-
1
def find_by_id(id, options={}, &optional_filter_block)
-
find(:id, id, options, &optional_filter_block)
-
end
-
-
##
-
# @!method all([kind = Capybara.default_selector], locator = nil, options = {})
-
#
-
# Find all elements on the page matching the given selector
-
# and options.
-
#
-
# Both XPath and CSS expressions are supported, but Capybara
-
# does not try to automatically distinguish between them. The
-
# following statements are equivalent:
-
#
-
# page.all(:css, 'a#person_123')
-
# page.all(:xpath, './/a[@id="person_123"]')
-
#
-
#
-
# If the type of selector is left out, Capybara uses
-
# {Capybara.default_selector}. It's set to :css by default.
-
#
-
# page.all("a#person_123")
-
#
-
# Capybara.default_selector = :xpath
-
# page.all('.//a[@id="person_123"]')
-
#
-
# The set of found elements can further be restricted by specifying
-
# options. It's possible to select elements by their text or visibility:
-
#
-
# page.all('a', text: 'Home')
-
# page.all('#menu li', visible: true)
-
#
-
# By default if no elements are found, an empty array is returned;
-
# however, expectations can be set on the number of elements to be found which
-
# will trigger Capybara's waiting behavior for the expectations to match.The
-
# expectations can be set using
-
#
-
# page.assert_selector('p#foo', count: 4)
-
# page.assert_selector('p#foo', maximum: 10)
-
# page.assert_selector('p#foo', minimum: 1)
-
# page.assert_selector('p#foo', between: 1..10)
-
#
-
# See {Capybara::Helpers#matches_count?} for additional information about
-
# count matching.
-
#
-
# @param [Symbol] kind Optional selector type (:css, :xpath, :field, etc.) - Defaults to Capybara.default_selector
-
# @param [String] locator The selector
-
# @option options [String, Regexp] text Only find elements which contain this text or match this regexp
-
# @option options [String, Boolean] exact_text (Capybara.exact_text) When String the string the elements contained text must match exactly, when Boolean controls whether the :text option must match exactly
-
# @option options [Boolean, Symbol] visible Only find elements with the specified visibility:
-
# * true - only finds visible elements.
-
# * false - finds invisible _and_ visible elements.
-
# * :all - same as false; finds visible and invisible elements.
-
# * :hidden - only finds invisible elements.
-
# * :visible - same as true; only finds visible elements.
-
# @option options [Integer] count Exact number of matches that are expected to be found
-
# @option options [Integer] maximum Maximum number of matches that are expected to be found
-
# @option options [Integer] minimum Minimum number of matches that are expected to be found
-
# @option options [Range] between Number of matches found must be within the given range
-
# @option options [Boolean] exact Control whether `is` expressions in the given XPath match exactly or partially
-
# @option options [Integer] wait (Capybara.default_max_wait_time) The time to wait for element count expectations to become true
-
# @overload all([kind = Capybara.default_selector], locator = nil, options = {})
-
# @overload all([kind = Capybara.default_selector], locator = nil, options = {}, &filter_block)
-
# @yieldparam element [Capybara::Node::Element] The element being considered for inclusion in the results
-
# @yieldreturn [Boolean] Should the element be considered in the results?
-
# @return [Capybara::Result] A collection of found elements
-
#
-
1
def all(*args, &optional_filter_block)
-
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
-
synchronize(query.wait) do
-
result = query.resolve_for(self)
-
raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?
-
result
-
end
-
end
-
1
alias_method :find_all, :all
-
-
##
-
#
-
# Find the first element on the page matching the given selector
-
# and options, or nil if no element matches. By default no waiting
-
# behavior occurs, however if {Capybara.wait_on_first_by_default} is set to true
-
# it will trigger Capybara's waiting behavior for a minimum of 1 matching element to be found and
-
# return the first. Waiting behavior can also be triggered by passing in any of the count
-
# expectation options.
-
#
-
# @overload first([kind], locator, options)
-
# @param [:css, :xpath] kind The type of selector
-
# @param [String] locator The selector
-
# @param [Hash] options Additional options; see {#all}
-
# @return [Capybara::Node::Element] The found element or nil
-
#
-
1
def first(*args, &optional_filter_block)
-
if Capybara.wait_on_first_by_default
-
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
-
args.push({minimum: 1}.merge(options))
-
end
-
all(*args, &optional_filter_block).first
-
rescue Capybara::ExpectationNotMet
-
nil
-
end
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
1
module Matchers
-
-
##
-
#
-
# Checks if a given selector is on the page or a descendant of the current node.
-
#
-
# page.has_selector?('p#foo')
-
# page.has_selector?(:xpath, './/p[@id="foo"]')
-
# page.has_selector?(:foo)
-
#
-
# By default it will check if the expression occurs at least once,
-
# but a different number can be specified.
-
#
-
# page.has_selector?('p.foo', count: 4)
-
#
-
# This will check if the expression occurs exactly 4 times.
-
#
-
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# page.has_selector?('li', text: 'Horse', visible: true)
-
#
-
# has_selector? can also accept XPath expressions generated by the
-
# XPath gem:
-
#
-
# page.has_selector?(:xpath, XPath.descendant(:p))
-
#
-
# @param (see Capybara::Node::Finders#all)
-
# @param args
-
# @option args [Integer] :count (nil) Number of times the text should occur
-
# @option args [Integer] :minimum (nil) Minimum number of times the text should occur
-
# @option args [Integer] :maximum (nil) Maximum number of times the text should occur
-
# @option args [Range] :between (nil) Range of times that should contain number of times text occurs
-
# @return [Boolean] If the expression exists
-
#
-
1
def has_selector?(*args, &optional_filter_block)
-
assert_selector(*args, &optional_filter_block)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
#
-
# Checks if a given selector is not on the page or a descendant of the current node.
-
# Usage is identical to Capybara::Node::Matchers#has_selector?
-
#
-
# @param (see Capybara::Node::Finders#has_selector?)
-
# @return [Boolean]
-
#
-
1
def has_no_selector?(*args, &optional_filter_block)
-
assert_no_selector(*args, &optional_filter_block)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
#
-
# Asserts that a given selector is on the page or a descendant of the current node.
-
#
-
# page.assert_selector('p#foo')
-
# page.assert_selector(:xpath, './/p[@id="foo"]')
-
# page.assert_selector(:foo)
-
#
-
# By default it will check if the expression occurs at least once,
-
# but a different number can be specified.
-
#
-
# page.assert_selector('p#foo', count: 4)
-
#
-
# This will check if the expression occurs exactly 4 times. See
-
# {Capybara::Node::Finders#all} for other available result size options.
-
#
-
# If a :count of 0 is specified, it will behave like {#assert_no_selector};
-
# however, use of that method is preferred over this one.
-
#
-
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# page.assert_selector('li', text: 'Horse', visible: true)
-
#
-
# `assert_selector` can also accept XPath expressions generated by the
-
# XPath gem:
-
#
-
# page.assert_selector(:xpath, XPath.descendant(:p))
-
#
-
# @param (see Capybara::Node::Finders#all)
-
# @option options [Integer] :count (nil) Number of times the expression should occur
-
# @raise [Capybara::ExpectationNotMet] If the selector does not exist
-
#
-
1
def assert_selector(*args, &optional_filter_block)
-
_verify_selector_result(args, optional_filter_block) do |result, query|
-
unless result.matches_count? && ((!result.empty?) || query.expects_none?)
-
raise Capybara::ExpectationNotMet, result.failure_message
-
end
-
end
-
end
-
-
# Asserts that all of the provided selectors are present on the given page
-
# or descendants of the current node. If options are provided, the assertion
-
# will check that each locator is present with those options as well (other than :wait).
-
#
-
# page.assert_all_of_selectors(:custom, 'Tom', 'Joe', visible: all)
-
# page.assert_all_of_selectors(:css, '#my_div', 'a.not_clicked')
-
#
-
# It accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# The :wait option applies to all of the selectors as a group, so all of the locators must be present
-
# within :wait (Defaults to Capybara.default_max_wait_time) seconds.
-
#
-
# @overload assert_all_of_selectors([kind = Capybara.default_selector], *locators, options = {})
-
#
-
1
def assert_all_of_selectors(*args, &optional_filter_block)
-
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
-
selector = if args.first.is_a?(Symbol) then args.shift else Capybara.default_selector end
-
wait = options.fetch(:wait, Capybara.default_max_wait_time)
-
synchronize(wait) do
-
args.each do |locator|
-
assert_selector(selector, locator, options, &optional_filter_block)
-
end
-
end
-
end
-
-
# Asserts that none of the provided selectors are present on the given page
-
# or descendants of the current node. If options are provided, the assertion
-
# will check that each locator is present with those options as well (other than :wait).
-
#
-
# page.assert_none_of_selectors(:custom, 'Tom', 'Joe', visible: all)
-
# page.assert_none_of_selectors(:css, '#my_div', 'a.not_clicked')
-
#
-
# It accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# The :wait option applies to all of the selectors as a group, so none of the locators must be present
-
# within :wait (Defaults to Capybara.default_max_wait_time) seconds.
-
#
-
# @overload assert_none_of_selectors([kind = Capybara.default_selector], *locators, options = {})
-
#
-
1
def assert_none_of_selectors(*args, &optional_filter_block)
-
options = if args.last.is_a?(Hash) then args.pop.dup else {} end
-
selector = if args.first.is_a?(Symbol) then args.shift else Capybara.default_selector end
-
wait = options.fetch(:wait, Capybara.default_max_wait_time)
-
synchronize(wait) do
-
args.each do |locator|
-
assert_no_selector(selector, locator, options, &optional_filter_block)
-
end
-
end
-
end
-
-
##
-
#
-
# Asserts that a given selector is not on the page or a descendant of the current node.
-
# Usage is identical to Capybara::Node::Matchers#assert_selector
-
#
-
# Query options such as :count, :minimum, :maximum, and :between are
-
# considered to be an integral part of the selector. This will return
-
# true, for example, if a page contains 4 anchors but the query expects 5:
-
#
-
# page.assert_no_selector('a', minimum: 1) # Found, raises Capybara::ExpectationNotMet
-
# page.assert_no_selector('a', count: 4) # Found, raises Capybara::ExpectationNotMet
-
# page.assert_no_selector('a', count: 5) # Not Found, returns true
-
#
-
# @param (see Capybara::Node::Finders#assert_selector)
-
# @raise [Capybara::ExpectationNotMet] If the selector exists
-
#
-
1
def assert_no_selector(*args, &optional_filter_block)
-
1
_verify_selector_result(args, optional_filter_block) do |result, query|
-
1
if result.matches_count? && ((!result.empty?) || query.expects_none?)
-
raise Capybara::ExpectationNotMet, result.negative_failure_message
-
end
-
end
-
end
-
1
alias_method :refute_selector, :assert_no_selector
-
-
##
-
#
-
# Checks if a given XPath expression is on the page or a descendant of the current node.
-
#
-
# page.has_xpath?('.//p[@id="foo"]')
-
#
-
# By default it will check if the expression occurs at least once,
-
# but a different number can be specified.
-
#
-
# page.has_xpath?('.//p[@id="foo"]', count: 4)
-
#
-
# This will check if the expression occurs exactly 4 times.
-
#
-
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# page.has_xpath?('.//li', text: 'Horse', visible: true)
-
#
-
# has_xpath? can also accept XPath expressions generate by the
-
# XPath gem:
-
#
-
# xpath = XPath.generate { |x| x.descendant(:p) }
-
# page.has_xpath?(xpath)
-
#
-
# @param [String] path An XPath expression
-
# @param options (see Capybara::Node::Finders#all)
-
# @option options [Integer] :count (nil) Number of times the expression should occur
-
# @return [Boolean] If the expression exists
-
#
-
1
def has_xpath?(path, options={}, &optional_filter_block)
-
has_selector?(:xpath, path, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if a given XPath expression is not on the page or a descendant of the current node.
-
# Usage is identical to Capybara::Node::Matchers#has_xpath?
-
#
-
# @param (see Capybara::Node::Finders#has_xpath?)
-
# @return [Boolean]
-
#
-
1
def has_no_xpath?(path, options={}, &optional_filter_block)
-
has_no_selector?(:xpath, path, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if a given CSS selector is on the page or a descendant of the current node.
-
#
-
# page.has_css?('p#foo')
-
#
-
# By default it will check if the selector occurs at least once,
-
# but a different number can be specified.
-
#
-
# page.has_css?('p#foo', count: 4)
-
#
-
# This will check if the selector occurs exactly 4 times.
-
#
-
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# page.has_css?('li', text: 'Horse', visible: true)
-
#
-
# @param [String] path A CSS selector
-
# @param options (see Capybara::Node::Finders#all)
-
# @option options [Integer] :count (nil) Number of times the selector should occur
-
# @return [Boolean] If the selector exists
-
#
-
1
def has_css?(path, options={}, &optional_filter_block)
-
has_selector?(:css, path, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if a given CSS selector is not on the page or a descendant of the current node.
-
# Usage is identical to Capybara::Node::Matchers#has_css?
-
#
-
# @param (see Capybara::Node::Finders#has_css?)
-
# @return [Boolean]
-
#
-
1
def has_no_css?(path, options={}, &optional_filter_block)
-
has_no_selector?(:css, path, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a link with the given
-
# text or id.
-
#
-
# @param [String] locator The text or id of a link to check for
-
# @param options
-
# @option options [String, Regexp] :href The value the href attribute must be
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_link?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:link, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no link with the given
-
# text or id.
-
#
-
# @param (see Capybara::Node::Finders#has_link?)
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_link?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:link, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a button with the given
-
# text, value or id.
-
#
-
# @param [String] locator The text, value or id of a button to check for
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_button?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:button, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no button with the given
-
# text, value or id.
-
#
-
# @param [String] locator The text, value or id of a button to check for
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_button?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:button, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a form field with the given
-
# label, name or id.
-
#
-
# For text fields and other textual fields, such as textareas and
-
# HTML5 email/url/etc. fields, it's possible to specify a :with
-
# option to specify the text the field should contain:
-
#
-
# page.has_field?('Name', with: 'Jonas')
-
#
-
# It is also possible to filter by the field type attribute:
-
#
-
# page.has_field?('Email', type: 'email')
-
#
-
# Note: 'textarea' and 'select' are valid type values, matching the associated tag names.
-
#
-
# @param [String] locator The label, name or id of a field to check for
-
# @option options [String, Regexp] :with The text content of the field or a Regexp to match
-
# @option options [String] :type The type attribute of the field
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:field, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no form field with the given
-
# label, name or id. See {Capybara::Node::Matchers#has_field?}.
-
#
-
# @param [String] locator The label, name or id of a field to check for
-
# @option options [String, Regexp] :with The text content of the field or a Regexp to match
-
# @option options [String] :type The type attribute of the field
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:field, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a radio button or
-
# checkbox with the given label, value or id, that is currently
-
# checked.
-
#
-
# @param [String] locator The label, name or id of a checked field
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_checked_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:field, locator, options.merge(checked: true), &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no radio button or
-
# checkbox with the given label, value or id, that is currently
-
# checked.
-
#
-
# @param [String] locator The label, name or id of a checked field
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_checked_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:field, locator, options.merge(checked: true), &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a radio button or
-
# checkbox with the given label, value or id, that is currently
-
# unchecked.
-
#
-
# @param [String] locator The label, name or id of an unchecked field
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_unchecked_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:field, locator, options.merge(unchecked: true), &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no radio button or
-
# checkbox with the given label, value or id, that is currently
-
# unchecked.
-
#
-
# @param [String] locator The label, name or id of an unchecked field
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_unchecked_field?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:field, locator, options.merge(unchecked: true), &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a select field with the
-
# given label, name or id.
-
#
-
# It can be specified which option should currently be selected:
-
#
-
# page.has_select?('Language', selected: 'German')
-
#
-
# For multiple select boxes, several options may be specified:
-
#
-
# page.has_select?('Language', selected: ['English', 'German'])
-
#
-
# It's also possible to check if the exact set of options exists for
-
# this select box:
-
#
-
# page.has_select?('Language', options: ['English', 'German', 'Spanish'])
-
#
-
# You can also check for a partial set of options:
-
#
-
# page.has_select?('Language', with_options: ['English', 'German'])
-
#
-
# @param [String] locator The label, name or id of a select box
-
# @option options [Array] :options Options which should be contained in this select box
-
# @option options [Array] :with_options Partial set of options which should be contained in this select box
-
# @option options [String, Array] :selected Options which should be selected
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_select?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:select, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no select field with the
-
# given label, name or id. See {Capybara::Node::Matchers#has_select?}.
-
#
-
# @param (see Capybara::Node::Matchers#has_select?)
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_select?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:select, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has a table with the given id
-
# or caption:
-
#
-
# page.has_table?('People')
-
#
-
# @param [String] locator The id or caption of a table
-
# @return [Boolean] Whether it exist
-
#
-
1
def has_table?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_selector?(:table, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the page or current node has no table with the given id
-
# or caption. See {Capybara::Node::Matchers#has_table?}.
-
#
-
# @param (see Capybara::Node::Matchers#has_table?)
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_table?(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
has_no_selector?(:table, locator, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Asserts that the current_node matches a given selector
-
#
-
# node.assert_matches_selector('p#foo')
-
# node.assert_matches_selector(:xpath, '//p[@id="foo"]')
-
# node.assert_matches_selector(:foo)
-
#
-
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
-
# such as :text and :visible.
-
#
-
# node.assert_matches_selector('li', text: 'Horse', visible: true)
-
#
-
# @param (see Capybara::Node::Finders#all)
-
# @raise [Capybara::ExpectationNotMet] If the selector does not match
-
#
-
1
def assert_matches_selector(*args, &optional_filter_block)
-
_verify_match_result(args, optional_filter_block) do |result|
-
raise Capybara::ExpectationNotMet, "Item does not match the provided selector" unless result.include? self
-
end
-
end
-
-
1
def assert_not_matches_selector(*args, &optional_filter_block)
-
_verify_match_result(args, optional_filter_block) do |result|
-
raise Capybara::ExpectationNotMet, 'Item matched the provided selector' if result.include? self
-
end
-
end
-
1
alias_method :refute_matches_selector, :assert_not_matches_selector
-
-
##
-
#
-
# Checks if the current node matches given selector
-
#
-
# @param (see Capybara::Node::Finders#has_selector?)
-
# @return [Boolean]
-
#
-
1
def matches_selector?(*args, &optional_filter_block)
-
assert_matches_selector(*args, &optional_filter_block)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
#
-
# Checks if the current node matches given XPath expression
-
#
-
# @param [String, XPath::Expression] xpath The XPath expression to match against the current code
-
# @return [Boolean]
-
#
-
1
def matches_xpath?(xpath, options={}, &optional_filter_block)
-
matches_selector?(:xpath, xpath, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the current node matches given CSS selector
-
#
-
# @param [String] css The CSS selector to match against the current code
-
# @return [Boolean]
-
#
-
1
def matches_css?(css, options={}, &optional_filter_block)
-
matches_selector?(:css, css, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the current node does not match given selector
-
# Usage is identical to Capybara::Node::Matchers#has_selector?
-
#
-
# @param (see Capybara::Node::Finders#has_selector?)
-
# @return [Boolean]
-
#
-
1
def not_matches_selector?(*args, &optional_filter_block)
-
assert_not_matches_selector(*args, &optional_filter_block)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
#
-
# Checks if the current node does not match given XPath expression
-
#
-
# @param [String, XPath::Expression] xpath The XPath expression to match against the current code
-
# @return [Boolean]
-
#
-
1
def not_matches_xpath?(xpath, options={}, &optional_filter_block)
-
not_matches_selector?(:xpath, xpath, options, &optional_filter_block)
-
end
-
-
##
-
#
-
# Checks if the current node does not match given CSS selector
-
#
-
# @param [String] css The CSS selector to match against the current code
-
# @return [Boolean]
-
#
-
1
def not_matches_css?(css, options={}, &optional_filter_block)
-
not_matches_selector?(:css, css, options, &optional_filter_block)
-
end
-
-
-
##
-
# Asserts that the page or current node has the given text content,
-
# ignoring any HTML tags.
-
#
-
# @!macro text_query_params
-
# @overload $0(type, text, options = {})
-
# @param [:all, :visible] type Whether to check for only visible or all text. If this parameter is missing or nil then we use the value of `Capybara.ignore_hidden_elements`, which defaults to `true`, corresponding to `:visible`.
-
# @param [String, Regexp] text The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
-
# @option options [Integer] :count (nil) Number of times the text is expected to occur
-
# @option options [Integer] :minimum (nil) Minimum number of times the text is expected to occur
-
# @option options [Integer] :maximum (nil) Maximum number of times the text is expected to occur
-
# @option options [Range] :between (nil) Range of times that is expected to contain number of times text occurs
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for text to eq/match given string/regexp argument
-
# @option options [Boolean] :exact (Capybara.exact_text) Whether text must be an exact match or just substring
-
# @overload $0(text, options = {})
-
# @param [String, Regexp] text The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
-
# @option options [Integer] :count (nil) Number of times the text is expected to occur
-
# @option options [Integer] :minimum (nil) Minimum number of times the text is expected to occur
-
# @option options [Integer] :maximum (nil) Maximum number of times the text is expected to occur
-
# @option options [Range] :between (nil) Range of times that is expected to contain number of times text occurs
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for text to eq/match given string/regexp argument
-
# @option options [Boolean] :exact (Capybara.exact_text) Whether text must be an exact match or just substring
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_text(*args)
-
2
_verify_text(args) do |count, query|
-
2
unless query.matches_count?(count) && ((count > 0) || query.expects_none?)
-
raise Capybara::ExpectationNotMet, query.failure_message
-
end
-
end
-
end
-
-
##
-
# Asserts that the page or current node doesn't have the given text content,
-
# ignoring any HTML tags.
-
#
-
# @macro text_query_params
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_no_text(*args)
-
_verify_text(args) do |count, query|
-
if query.matches_count?(count) && ((count > 0) || query.expects_none?)
-
raise Capybara::ExpectationNotMet, query.negative_failure_message
-
end
-
end
-
end
-
-
##
-
# Checks if the page or current node has the given text content,
-
# ignoring any HTML tags.
-
#
-
# Whitespaces are normalized in both node's text and passed text parameter.
-
# Note that whitespace isn't normalized in passed regexp as normalizing whitespace
-
# in regexp isn't easy and doesn't seem to be worth it.
-
#
-
# By default it will check if the text occurs at least once,
-
# but a different number can be specified.
-
#
-
# page.has_text?('lorem ipsum', between: 2..4)
-
#
-
# This will check if the text occurs from 2 to 4 times.
-
#
-
# @macro text_query_params
-
# @return [Boolean] Whether it exists
-
#
-
1
def has_text?(*args)
-
assert_text(*args)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
1
alias_method :has_content?, :has_text?
-
-
##
-
# Checks if the page or current node does not have the given text
-
# content, ignoring any HTML tags and normalizing whitespace.
-
#
-
# @macro text_query_params
-
# @return [Boolean] Whether it doesn't exist
-
#
-
1
def has_no_text?(*args)
-
assert_no_text(*args)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
1
alias_method :has_no_content?, :has_no_text?
-
-
1
def ==(other)
-
self.eql?(other) || (other.respond_to?(:base) && base == other.base)
-
end
-
-
1
private
-
-
1
def _verify_selector_result(query_args, optional_filter_block, &result_block)
-
1
query = Capybara::Queries::SelectorQuery.new(*query_args, &optional_filter_block)
-
1
synchronize(query.wait) do
-
1
result = query.resolve_for(self)
-
1
result_block.call(result, query)
-
end
-
1
return true
-
end
-
-
1
def _verify_match_result(query_args, optional_filter_block, &result_block)
-
query = Capybara::Queries::MatchQuery.new(*query_args, &optional_filter_block)
-
synchronize(query.wait) do
-
result = query.resolve_for(self.query_scope)
-
result_block.call(result)
-
end
-
return true
-
end
-
-
1
def _verify_text(query_args)
-
2
query = Capybara::Queries::TextQuery.new(*query_args)
-
2
synchronize(query.wait) do
-
2
count = query.resolve_for(self)
-
2
yield(count, query)
-
end
-
2
return true
-
end
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Node
-
-
##
-
#
-
# A {Capybara::Node::Simple} is a simpler version of {Capybara::Node::Base} which
-
# includes only {Capybara::Node::Finders} and {Capybara::Node::Matchers} and does
-
# not include {Capybara::Node::Actions}. This type of node is returned when
-
# using {Capybara.string}.
-
#
-
# It is useful in that it does not require a session, an application or a driver,
-
# but can still use Capybara's finders and matchers on any string that contains HTML.
-
#
-
1
class Simple
-
1
include Capybara::Node::Finders
-
1
include Capybara::Node::Matchers
-
1
include Capybara::Node::DocumentMatchers
-
-
1
attr_reader :native
-
-
1
def initialize(native)
-
native = Capybara::HTML(native) if native.is_a?(String)
-
@native = native
-
end
-
-
##
-
#
-
# @return [String] The text of the element
-
#
-
1
def text(type=nil)
-
native.text
-
end
-
-
##
-
#
-
# Retrieve the given attribute
-
#
-
# element[:title] # => HTML title attribute
-
#
-
# @param [Symbol] name The attribute name to retrieve
-
# @return [String] The value of the attribute
-
#
-
1
def [](name)
-
attr_name = name.to_s
-
if attr_name == 'value'
-
value
-
elsif 'input' == tag_name and 'checkbox' == native[:type] and 'checked' == attr_name
-
native['checked'] == 'checked'
-
else
-
native[attr_name]
-
end
-
end
-
-
##
-
#
-
# @return [String] The tag name of the element
-
#
-
1
def tag_name
-
native.node_name
-
end
-
-
##
-
#
-
# An XPath expression describing where on the page the element can be found
-
#
-
# @return [String] An XPath expression
-
#
-
1
def path
-
native.path
-
end
-
-
##
-
#
-
# @return [String] The value of the form element
-
#
-
1
def value
-
if tag_name == 'textarea'
-
native['_capybara_raw_value']
-
elsif tag_name == 'select'
-
if native['multiple'] == 'multiple'
-
native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
-
else
-
option = native.xpath(".//option[@selected='selected']").first || native.xpath(".//option").first
-
option[:value] || option.content if option
-
end
-
elsif tag_name == 'input' && %w(radio checkbox).include?(native[:type])
-
native[:value] || 'on'
-
else
-
native[:value]
-
end
-
end
-
-
##
-
#
-
# Whether or not the element is visible. Does not support CSS, so
-
# the result may be inaccurate.
-
#
-
# @param [Boolean] check_ancestors Whether to inherit visibility from ancestors
-
# @return [Boolean] Whether the element is visible
-
#
-
1
def visible?(check_ancestors = true)
-
return false if (tag_name == 'input') && (native[:type]=="hidden")
-
-
if check_ancestors
-
#check size because oldest supported nokogiri doesnt support xpath boolean() function
-
native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or @hidden or name()='script' or name()='head']").size() == 0
-
else
-
#no need for an xpath if only checking the current element
-
!(native.has_attribute?('hidden') || (native[:style] =~ /display:\s?none/) || %w(script head).include?(tag_name))
-
end
-
end
-
-
##
-
#
-
# Whether or not the element is checked.
-
#
-
# @return [Boolean] Whether the element is checked
-
#
-
1
def checked?
-
native.has_attribute?('checked')
-
end
-
-
##
-
#
-
# Whether or not the element is disabled.
-
#
-
# @return [Boolean] Whether the element is disabled
-
1
def disabled?
-
native.has_attribute?('disabled')
-
end
-
-
##
-
#
-
# Whether or not the element is selected.
-
#
-
# @return [Boolean] Whether the element is selected
-
#
-
1
def selected?
-
native.has_attribute?('selected')
-
end
-
-
1
def synchronize(seconds=nil)
-
yield # simple nodes don't need to wait
-
end
-
-
1
def allow_reload!
-
# no op
-
end
-
-
##
-
#
-
# @return [String] The title of the document
-
1
def title
-
if native.respond_to? :title
-
native.title
-
else
-
#old versions of nokogiri don't have #title - remove in 3.0
-
native.xpath('/html/head/title | /html/title').first.text
-
end
-
end
-
-
1
def inspect
-
%(#<Capybara::Node::Simple tag="#{tag_name}" path="#{path}">)
-
end
-
-
# @api private
-
1
def find_css(css)
-
native.css(css)
-
end
-
-
# @api private
-
1
def find_xpath(xpath)
-
native.xpath(xpath)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
# @api private
-
1
module Queries
-
1
class BaseQuery
-
1
COUNT_KEYS = [:count, :minimum, :maximum, :between]
-
-
1
attr_reader :options
-
-
1
def wait
-
81
self.class.wait(options)
-
end
-
-
1
def self.wait(options)
-
81
options.fetch(:wait, Capybara.default_max_wait_time) || 0
-
end
-
-
##
-
#
-
# Checks if a count of 0 is valid for the query
-
# Returns false if query does not have any count options specified.
-
#
-
1
def expects_none?
-
5
if COUNT_KEYS.any? { |k| options.has_key? k }
-
matches_count?(0)
-
else
-
1
false
-
end
-
end
-
-
##
-
#
-
# Checks if the given count matches the query count options.
-
# Defaults to true if no count options are specified. If multiple
-
# count options exist, it tests that all conditions are met;
-
# however, if :count is specified, all other options are ignored.
-
#
-
# @param [Integer] count The actual number. Should be coercible via Integer()
-
#
-
1
def matches_count?(count)
-
2
return (Integer(options[:count]) == count) if options[:count]
-
2
return false if options[:maximum] && (Integer(options[:maximum]) < count)
-
2
return false if options[:minimum] && (Integer(options[:minimum]) > count)
-
2
return false if options[:between] && !(options[:between] === count)
-
2
return true
-
end
-
-
##
-
#
-
# Generates a failure message from the query description and count options.
-
#
-
1
def failure_message
-
String.new("expected to find #{description}") << count_message
-
end
-
-
1
def negative_failure_message
-
String.new("expected not to find #{description}") << count_message
-
end
-
-
1
private
-
-
1
def count_message
-
message = String.new()
-
if options[:count]
-
message << " #{options[:count]} #{Capybara::Helpers.declension('time', 'times', options[:count])}"
-
elsif options[:between]
-
message << " between #{options[:between].first} and #{options[:between].last} times"
-
elsif options[:maximum]
-
message << " at most #{options[:maximum]} #{Capybara::Helpers.declension('time', 'times', options[:maximum])}"
-
elsif options[:minimum]
-
message << " at least #{options[:minimum]} #{Capybara::Helpers.declension('time', 'times', options[:minimum])}"
-
end
-
message
-
end
-
-
1
def assert_valid_keys
-
81
invalid_keys = @options.keys - valid_keys
-
81
unless invalid_keys.empty?
-
invalid_names = invalid_keys.map(&:inspect).join(", ")
-
valid_names = valid_keys.map(&:inspect).join(", ")
-
raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'addressable/uri'
-
-
1
module Capybara
-
# @api private
-
1
module Queries
-
1
class CurrentPathQuery < BaseQuery
-
1
def initialize(expected_path, options = {})
-
@expected_path = expected_path
-
@options = {
-
url: false,
-
only_path: false }.merge(options)
-
assert_valid_keys
-
end
-
-
1
def resolves_for?(session)
-
@actual_path = if options[:url]
-
session.current_url
-
else
-
uri = ::Addressable::URI.parse(session.current_url)
-
-
if options[:only_path]
-
uri.path unless uri.nil? # Ensure the parsed url isn't nil.
-
else
-
uri.request_uri unless uri.nil? # Ensure the parsed url isn't nil.
-
end
-
end
-
-
if @expected_path.is_a? Regexp
-
@actual_path.match(@expected_path)
-
else
-
::Addressable::URI.parse(@expected_path) == Addressable::URI.parse(@actual_path)
-
end
-
end
-
-
1
def failure_message
-
failure_message_helper
-
end
-
-
1
def negative_failure_message
-
failure_message_helper(' not')
-
end
-
-
1
private
-
-
1
def failure_message_helper(negated = '')
-
verb = (@expected_path.is_a?(Regexp))? 'match' : 'equal'
-
"expected #{@actual_path.inspect}#{negated} to #{verb} #{@expected_path.inspect}"
-
end
-
-
1
def valid_keys
-
[:wait, :url, :only_path]
-
end
-
-
1
def assert_valid_keys
-
super
-
if options[:url] && options[:only_path]
-
raise ArgumentError, "the :url and :only_path options cannot both be true"
-
end
-
end
-
end
-
end
-
end
-
1
module Capybara
-
1
module Queries
-
1
class MatchQuery < Capybara::Queries::SelectorQuery
-
1
def visible
-
if options.has_key?(:visible)
-
super
-
else
-
:all
-
end
-
end
-
-
1
private
-
-
1
def valid_keys
-
super - COUNT_KEYS
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module Queries
-
1
class SelectorQuery < Queries::BaseQuery
-
1
attr_accessor :selector, :locator, :options, :expression, :find, :negative
-
-
1
VALID_KEYS = COUNT_KEYS + [:text, :id, :class, :visible, :exact, :exact_text, :match, :wait, :filter_set]
-
1
VALID_MATCH = [:first, :smart, :prefer_exact, :one]
-
-
1
def initialize(*args, &filter_block)
-
79
@options = if args.last.is_a?(Hash) then args.pop.dup else {} end
-
79
@filter_block = filter_block
-
-
79
if args[0].is_a?(Symbol)
-
79
@selector = Selector.all.fetch(args.shift) do |selector_type|
-
warn "Unknown selector type (:#{selector_type}), defaulting to :#{Capybara.default_selector} - This will raise an exception in a future version of Capybara"
-
nil
-
end
-
79
@locator = args.shift
-
else
-
@selector = Selector.all.values.find { |s| s.match?(args[0]) }
-
@locator = args.shift
-
end
-
79
@selector ||= Selector.all[Capybara.default_selector]
-
-
79
warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
-
-
# for compatibility with Capybara 2.0
-
79
if Capybara.exact_options and @selector == Selector.all[:option]
-
@options[:exact] = true
-
end
-
-
79
@expression = @selector.call(@locator, @options)
-
-
79
warn_exact_usage
-
-
79
assert_valid_keys
-
end
-
-
1
def name; selector.name; end
-
1
def label; selector.label or selector.name; end
-
-
1
def description
-
@description = String.new("#{label} #{locator.inspect}")
-
@description << " with#{" exact" if exact_text === true} text #{options[:text].inspect}" if options[:text]
-
@description << " with exact text #{options[:exact_text]}" if options[:exact_text].is_a?(String)
-
@description << " with id #{options[:id]}" if options[:id]
-
@description << " with classes #{Array(options[:class]).join(',')}]" if options[:class]
-
@description << selector.description(options)
-
@description << " that also matches the custom filter block" if @filter_block
-
@description
-
end
-
-
1
def matches_filters?(node)
-
78
if options[:text]
-
regexp = if options[:text].is_a?(Regexp)
-
options[:text]
-
else
-
if exact_text === true
-
"\\A#{Regexp.escape(options[:text].to_s)}\\z"
-
else
-
Regexp.escape(options[:text].to_s)
-
end
-
end
-
text_visible = visible
-
text_visible = :all if text_visible == :hidden
-
return false if not node.text(text_visible).match(regexp)
-
end
-
-
78
if exact_text.is_a?(String)
-
regexp = "\\A#{Regexp.escape(options[:exact_text])}\\z"
-
text_visible = visible
-
text_visible = :all if text_visible == :hidden
-
return false if not node.text(text_visible).match(regexp)
-
end
-
-
78
case visible
-
78
when :visible then return false unless node.visible?
-
when :hidden then return false if node.visible?
-
end
-
-
78
res = query_filters.all? do |name, filter|
-
198
if options.has_key?(name)
-
filter.matches?(node, options[name])
-
198
elsif filter.default?
-
76
filter.matches?(node, filter.default)
-
else
-
122
true
-
end
-
end
-
-
78
res &&= Capybara.using_wait_time(0){ @filter_block.call(node)} unless @filter_block.nil?
-
78
res
-
end
-
-
1
def visible
-
156
case (vis = options.fetch(:visible){ @selector.default_visibility })
-
78
when true then :visible
-
when false then :all
-
else vis
-
end
-
end
-
-
1
def exact?
-
3
return false if !supports_exact?
-
1
options.fetch(:exact, Capybara.exact)
-
end
-
-
1
def match
-
313
options.fetch(:match, Capybara.match)
-
end
-
-
1
def xpath(exact=nil)
-
79
exact = self.exact? if exact.nil?
-
79
expr = if @expression.respond_to?(:to_xpath) and exact
-
76
@expression.to_xpath(:exact)
-
else
-
3
@expression.to_s
-
end
-
79
filtered_xpath(expr)
-
end
-
-
1
def css
-
filtered_css(@expression)
-
end
-
-
# @api private
-
1
def resolve_for(node, exact = nil)
-
79
node.synchronize do
-
79
children = if selector.format == :css
-
node.find_css(self.css)
-
else
-
79
node.find_xpath(self.xpath(exact))
-
end.map do |child|
-
78
if node.is_a?(Capybara::Node::Base)
-
78
Capybara::Node::Element.new(node.session, child, node, self)
-
else
-
Capybara::Node::Simple.new(child)
-
end
-
end
-
79
Capybara::Result.new(children, self)
-
end
-
end
-
-
# @api private
-
1
def supports_exact?
-
81
@expression.respond_to? :to_xpath
-
end
-
-
1
private
-
-
1
def valid_keys
-
79
VALID_KEYS + custom_keys
-
end
-
-
1
def query_filters
-
157
if options.has_key?(:filter_set)
-
Capybara::Selector::FilterSet.all[options[:filter_set]].filters
-
else
-
157
@selector.custom_filters
-
end
-
end
-
-
1
def custom_keys
-
79
@custom_keys ||= query_filters.keys + @selector.expression_filters
-
end
-
-
1
def assert_valid_keys
-
79
super
-
79
unless VALID_MATCH.include?(match)
-
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
-
end
-
end
-
-
1
def filtered_xpath(expr)
-
79
if options.has_key?(:id) || options.has_key?(:class)
-
expr = "(#{expr})"
-
expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.has_key?(:id) && !custom_keys.include?(:id)
-
if options.has_key?(:class) && !custom_keys.include?(:class)
-
class_xpath = Array(options[:class]).map do |klass|
-
"contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
-
end.join(" and ")
-
expr = "#{expr}[#{class_xpath}]"
-
end
-
end
-
79
expr
-
end
-
-
1
def filtered_css(expr)
-
if options.has_key?(:id) || options.has_key?(:class)
-
css_selectors = expr.split(',').map(&:rstrip)
-
expr = css_selectors.map do |sel|
-
sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.has_key?(:id) && !custom_keys.include?(:id)
-
sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}"}.join if options.has_key?(:class) && !custom_keys.include?(:class)
-
sel
-
end.join(", ")
-
end
-
expr
-
end
-
-
1
def warn_exact_usage
-
79
if options.has_key?(:exact) && !supports_exact?
-
warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression.to_s}\" has no effect."
-
end
-
end
-
-
1
def exact_text
-
78
exact_text = options.fetch(:exact_text, Capybara.exact_text)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
# @api private
-
1
module Queries
-
1
class TextQuery < BaseQuery
-
1
def initialize(*args)
-
2
@type = (args.first.is_a?(Symbol) || args.first.nil?) ? args.shift : nil
-
2
@type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
-
2
@expected_text, @options = args
-
2
unless @expected_text.is_a?(Regexp)
-
2
@expected_text = Capybara::Helpers.normalize_whitespace(@expected_text)
-
end
-
2
@options ||= {}
-
2
@search_regexp = Capybara::Helpers.to_regexp(@expected_text, nil, exact?)
-
2
assert_valid_keys
-
end
-
-
1
def resolve_for(node)
-
2
@node = node
-
2
@actual_text = text(node, @type)
-
2
@count = @actual_text.scan(@search_regexp).size
-
end
-
-
1
def failure_message
-
super << build_message(true)
-
end
-
-
1
def negative_failure_message
-
super << build_message(false)
-
end
-
-
1
def description
-
if @expected_text.is_a?(Regexp)
-
"text matching #{@expected_text.inspect}"
-
else
-
"#{"exact " if exact?}text #{@expected_text.inspect}"
-
end
-
end
-
-
1
private
-
-
1
def exact?
-
2
options.fetch(:exact, Capybara.exact_text)
-
end
-
-
1
def build_message(report_on_invisible)
-
message = String.new()
-
unless (COUNT_KEYS & @options.keys).empty?
-
message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
-
end
-
message << " in #{@actual_text.inspect}"
-
-
details_message = []
-
-
if @node and !@expected_text.is_a? Regexp
-
insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, Regexp::IGNORECASE)
-
insensitive_count = @actual_text.scan(insensitive_regexp).size
-
if insensitive_count != @count
-
details_message << "it was found #{insensitive_count} #{Capybara::Helpers.declension("time", "times", insensitive_count)} using a case insensitive search"
-
end
-
end
-
-
if @node and check_visible_text? and report_on_invisible
-
begin
-
invisible_text = text(@node, :all)
-
invisible_count = invisible_text.scan(@search_regexp).size
-
if invisible_count != @count
-
details_message << ". it was found #{invisible_count} #{Capybara::Helpers.declension("time", "times", invisible_count)} including non-visible text"
-
end
-
rescue
-
# An error getting the non-visible text (if element goes out of scope) should not affect the response
-
end
-
end
-
-
message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
-
-
message
-
end
-
-
1
def valid_keys
-
2
COUNT_KEYS + [:wait, :exact]
-
end
-
-
1
def check_visible_text?
-
@type == :visible
-
end
-
-
1
def text(node, query_type)
-
2
Capybara::Helpers.normalize_whitespace(node.text(query_type))
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
# @api private
-
1
module Queries
-
1
class TitleQuery < BaseQuery
-
1
def initialize(expected_title, options = {})
-
@expected_title = expected_title
-
@options = options
-
unless @expected_title.is_a?(Regexp)
-
@expected_title = Capybara::Helpers.normalize_whitespace(@expected_title)
-
end
-
@search_regexp = Capybara::Helpers.to_regexp(@expected_title, nil, options.fetch(:exact, false))
-
assert_valid_keys
-
end
-
-
1
def resolves_for?(node)
-
@actual_title = node.title
-
@actual_title.match(@search_regexp)
-
end
-
-
1
def failure_message
-
failure_message_helper
-
end
-
-
1
def negative_failure_message
-
failure_message_helper(' not')
-
end
-
-
1
private
-
-
1
def failure_message_helper(negated = '')
-
verb = (@expected_title.is_a?(Regexp))? 'match' : 'include'
-
"expected #{@actual_title.inspect}#{negated} to #{verb} #{@expected_title.inspect}"
-
end
-
-
1
def valid_keys
-
[:wait, :exact]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/queries/selector_query'
-
1
module Capybara
-
# @deprecated This class and its methods are not supposed to be used by users of Capybara's public API.
-
# It may be removed in future versions of Capybara.
-
1
Query = Queries::SelectorQuery
-
end
-
# frozen_string_literal: true
-
1
class Capybara::RackTest::Browser
-
1
include ::Rack::Test::Methods
-
-
1
attr_reader :driver
-
1
attr_accessor :current_host
-
-
1
def initialize(driver)
-
@driver = driver
-
end
-
-
1
def app
-
driver.app
-
end
-
-
1
def options
-
driver.options
-
end
-
-
1
def visit(path, attributes = {})
-
reset_host!
-
process_and_follow_redirects(:get, path, attributes)
-
end
-
-
1
def submit(method, path, attributes)
-
path = request_path if not path or path.empty?
-
process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
-
end
-
-
1
def follow(method, path, attributes = {})
-
return if path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
-
process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
-
end
-
-
1
def process_and_follow_redirects(method, path, attributes = {}, env = {})
-
process(method, path, attributes, env)
-
if driver.follow_redirects?
-
driver.redirect_limit.times do
-
process(:get, last_response["Location"], {}, env) if last_response.redirect?
-
end
-
raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
-
end
-
end
-
-
1
def process(method, path, attributes = {}, env = {})
-
new_uri = URI.parse(path)
-
method.downcase! unless method.is_a? Symbol
-
-
new_uri.path = request_path if path.start_with?("?")
-
new_uri.path = "/" if new_uri.path.empty?
-
new_uri.path = request_path.sub(%r(/[^/]*$), '/') + new_uri.path unless new_uri.path.start_with?('/')
-
new_uri.scheme ||= @current_scheme
-
new_uri.host ||= @current_host
-
new_uri.port ||= @current_port unless new_uri.default_port == @current_port
-
-
@current_scheme = new_uri.scheme
-
@current_host = new_uri.host
-
@current_port = new_uri.port
-
-
reset_cache!
-
send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
-
end
-
-
1
def current_url
-
last_request.url
-
rescue Rack::Test::Error
-
""
-
end
-
-
1
def reset_host!
-
uri = URI.parse(Capybara.app_host || Capybara.default_host)
-
@current_scheme = uri.scheme
-
@current_host = uri.host
-
@current_port = uri.port
-
end
-
-
1
def reset_cache!
-
@dom = nil
-
end
-
-
1
def dom
-
@dom ||= Capybara::HTML(html)
-
end
-
-
1
def find(format, selector)
-
if format==:css
-
dom.css(selector, Capybara::RackTest::CSSHandlers.new)
-
else
-
dom.xpath(selector)
-
end.map { |node| Capybara::RackTest::Node.new(self, node) }
-
end
-
-
1
def html
-
last_response.body
-
rescue Rack::Test::Error
-
""
-
end
-
-
1
def title
-
if dom.respond_to? :title
-
dom.title
-
else
-
#old versions of nokogiri don't have #title - remove in 3.0
-
dom.xpath('/html/head/title | /html/title').first.text
-
end
-
end
-
-
1
protected
-
-
1
def build_rack_mock_session
-
reset_host! unless current_host
-
Rack::MockSession.new(app, current_host)
-
end
-
-
1
def request_path
-
last_request.path
-
rescue Rack::Test::Error
-
"/"
-
end
-
end
-
# frozen_string_literal: true
-
1
class Capybara::RackTest::CSSHandlers < BasicObject
-
1
include ::Kernel
-
-
1
def disabled list
-
list.find_all { |node| node.has_attribute? 'disabled' }
-
end
-
1
def enabled list
-
list.find_all { |node| !node.has_attribute? 'disabled' }
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'rack/test'
-
1
require 'rack/utils'
-
1
require 'mime/types'
-
1
require 'nokogiri'
-
1
require 'cgi'
-
-
1
class Capybara::RackTest::Driver < Capybara::Driver::Base
-
1
DEFAULT_OPTIONS = {
-
respect_data_method: false,
-
follow_redirects: true,
-
redirect_limit: 5
-
}
-
1
attr_reader :app, :options
-
-
1
def initialize(app, options={})
-
raise ArgumentError, "rack-test requires a rack application, but none was given" unless app
-
@app = app
-
@options = DEFAULT_OPTIONS.merge(options)
-
end
-
-
1
def browser
-
@browser ||= Capybara::RackTest::Browser.new(self)
-
end
-
-
1
def follow_redirects?
-
@options[:follow_redirects]
-
end
-
-
1
def redirect_limit
-
@options[:redirect_limit]
-
end
-
-
1
def response
-
browser.last_response
-
end
-
-
1
def request
-
browser.last_request
-
end
-
-
1
def visit(path, attributes = {})
-
browser.visit(path, attributes)
-
end
-
-
1
def submit(method, path, attributes)
-
browser.submit(method, path, attributes)
-
end
-
-
1
def follow(method, path, attributes = {})
-
browser.follow(method, path, attributes)
-
end
-
-
1
def current_url
-
browser.current_url
-
end
-
-
1
def response_headers
-
response.headers
-
end
-
-
1
def status_code
-
response.status
-
end
-
-
1
def find_xpath(selector)
-
browser.find(:xpath, selector)
-
end
-
-
1
def find_css(selector)
-
browser.find(:css,selector)
-
end
-
-
1
def html
-
browser.html
-
end
-
-
1
def dom
-
browser.dom
-
end
-
-
1
def title
-
browser.title
-
end
-
-
1
def reset!
-
@browser = nil
-
end
-
-
# @deprecated This method is being removed
-
1
def browser_initialized?
-
super && !@browser.nil?
-
end
-
-
1
def get(*args, &block); browser.get(*args, &block); end
-
1
def post(*args, &block); browser.post(*args, &block); end
-
1
def put(*args, &block); browser.put(*args, &block); end
-
1
def delete(*args, &block); browser.delete(*args, &block); end
-
1
def header(key, value); browser.header(key, value); end
-
end
-
# frozen_string_literal: true
-
1
class Capybara::RackTest::Form < Capybara::RackTest::Node
-
# This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for
-
# the class specifically when determining whether to construct the request as multipart.
-
# That check should be based solely on the form element's 'enctype' attribute value,
-
# which should probably be provided to Rack::Test in its non-GET request methods.
-
1
class NilUploadedFile < Rack::Test::UploadedFile
-
1
def initialize
-
@empty_file = Tempfile.new("nil_uploaded_file")
-
@empty_file.close
-
end
-
-
1
def original_filename; ""; end
-
1
def content_type; "application/octet-stream"; end
-
1
def path; @empty_file.path; end
-
end
-
-
1
def params(button)
-
params = make_params
-
-
form_element_types=[:input, :select, :textarea]
-
form_elements_xpath=XPath.generate do |x|
-
xpath=x.descendant(*form_element_types).where(~x.attr(:form))
-
xpath=xpath.union(x.anywhere(*form_element_types).where(x.attr(:form) == native[:id])) if native[:id]
-
xpath.where(~x.attr(:disabled))
-
end.to_s
-
-
native.xpath(form_elements_xpath).map do |field|
-
case field.name
-
when 'input'
-
if %w(radio checkbox).include? field['type']
-
if field['checked']
-
node=Capybara::RackTest::Node.new(self.driver, field)
-
merge_param!(params, field['name'].to_s, node.value.to_s)
-
end
-
elsif %w(submit image).include? field['type']
-
# TO DO identify the click button here (in document order, rather
-
# than leaving until the end of the params)
-
elsif field['type'] =='file'
-
if multipart?
-
file = \
-
if (value = field['value']).to_s.empty?
-
NilUploadedFile.new
-
else
-
types = MIME::Types.type_for(value)
-
content_type = types.sort_by.with_index { |type, idx| [type.obsolete? ? 1 : 0, idx] }.first.to_s
-
Rack::Test::UploadedFile.new(value, content_type)
-
end
-
merge_param!(params, field['name'].to_s, file)
-
else
-
merge_param!(params, field['name'].to_s, File.basename(field['value'].to_s))
-
end
-
else
-
merge_param!(params, field['name'].to_s, field['value'].to_s)
-
end
-
when 'select'
-
if field['multiple'] == 'multiple'
-
options = field.xpath(".//option[@selected]")
-
options.each do |option|
-
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s)
-
end
-
else
-
option = field.xpath(".//option[@selected]").first
-
option ||= field.xpath('.//option').first
-
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s) if option
-
end
-
when 'textarea'
-
merge_param!(params, field['name'].to_s, field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
-
end
-
end
-
merge_param!(params, button[:name], button[:value] || "") if button[:name]
-
-
params.to_params_hash
-
end
-
-
1
def submit(button)
-
action = (button && button['formaction']) || native['action']
-
method = (button && button['formmethod']) || request_method
-
driver.submit(method, action.to_s, params(button))
-
end
-
-
1
def multipart?
-
self[:enctype] == "multipart/form-data"
-
end
-
-
1
private
-
-
1
class ParamsHash < Hash
-
1
def to_params_hash
-
self
-
end
-
end
-
-
1
def request_method
-
self[:method] =~ /post/i ? :post : :get
-
end
-
-
1
def merge_param!(params, key, value)
-
if Rack::Utils.respond_to?(:default_query_parser)
-
Rack::Utils.default_query_parser.normalize_params(params, key, value, Rack::Utils.param_depth_limit)
-
else
-
Rack::Utils.normalize_params(params, key, value)
-
end
-
end
-
-
1
def make_params
-
if Rack::Utils.respond_to?(:default_query_parser)
-
Rack::Utils.default_query_parser.make_params
-
else
-
ParamsHash.new
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
class Capybara::RackTest::Node < Capybara::Driver::Node
-
1
def all_text
-
Capybara::Helpers.normalize_whitespace(native.text)
-
end
-
-
1
def visible_text
-
Capybara::Helpers.normalize_whitespace(unnormalized_text)
-
end
-
-
1
def [](name)
-
string_node[name]
-
end
-
-
1
def value
-
string_node.value
-
end
-
-
1
def set(value)
-
if (Array === value) && !multiple?
-
raise TypeError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
-
end
-
-
if radio?
-
set_radio(value)
-
elsif checkbox?
-
set_checkbox(value)
-
elsif input_field?
-
set_input(value)
-
elsif textarea?
-
if self[:readonly]
-
warn "Attempt to set readonly element with value: #{value} \n * This will raise an exception in a future version of Capybara"
-
else
-
native['_capybara_raw_value'] = value.to_s
-
end
-
end
-
end
-
-
1
def select_option
-
return if disabled?
-
if select_node['multiple'] != 'multiple'
-
select_node.find_xpath(".//option[@selected]").each { |node| node.native.remove_attribute("selected") }
-
end
-
native["selected"] = 'selected'
-
end
-
-
1
def unselect_option
-
if select_node['multiple'] != 'multiple'
-
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
-
end
-
native.remove_attribute('selected')
-
end
-
-
1
def click
-
if tag_name == 'a' && !self[:href].nil?
-
method = self["data-method"] if driver.options[:respect_data_method]
-
method ||= :get
-
driver.follow(method, self[:href].to_s)
-
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
-
((tag_name == 'button') and type.nil? or type == "submit")
-
associated_form = form
-
Capybara::RackTest::Form.new(driver, associated_form).submit(self) if associated_form
-
elsif (tag_name == 'label')
-
labelled_control = if native[:for]
-
find_xpath("//input[@id='#{native[:for]}']").first
-
else
-
find_xpath(".//input").first
-
end
-
-
if labelled_control && (labelled_control.checkbox? || labelled_control.radio?)
-
labelled_control.set(!labelled_control.checked?)
-
end
-
end
-
end
-
-
1
def tag_name
-
native.node_name
-
end
-
-
1
def visible?
-
string_node.visible?
-
end
-
-
1
def checked?
-
string_node.checked?
-
end
-
-
1
def selected?
-
string_node.selected?
-
end
-
-
1
def disabled?
-
if %w(option optgroup).include? tag_name
-
string_node.disabled? || find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
-
else
-
string_node.disabled? || !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
-
end
-
end
-
-
1
def path
-
native.path
-
end
-
-
1
def find_xpath(locator)
-
native.xpath(locator).map { |n| self.class.new(driver, n) }
-
end
-
-
1
def find_css(locator)
-
native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |n| self.class.new(driver, n) }
-
end
-
-
1
def ==(other)
-
native == other.native
-
end
-
-
1
protected
-
-
1
def unnormalized_text(check_ancestor_visibility = true)
-
if !string_node.visible?(check_ancestor_visibility)
-
''
-
elsif native.text?
-
native.text
-
elsif native.element?
-
native.children.map do |child|
-
Capybara::RackTest::Node.new(driver, child).unnormalized_text(false)
-
end.join
-
else
-
''
-
end
-
end
-
-
1
private
-
-
1
def string_node
-
@string_node ||= Capybara::Node::Simple.new(native)
-
end
-
-
# a reference to the select node if this is an option node
-
1
def select_node
-
find_xpath('./ancestor::select').first
-
end
-
-
1
def type
-
native[:type]
-
end
-
-
1
def form
-
if native[:form]
-
native.xpath("//form[@id='#{native[:form]}']").first
-
else
-
native.ancestors('form').first
-
end
-
end
-
-
1
def set_radio(_value)
-
other_radios_xpath = XPath.generate { |x| x.anywhere(:input)[x.attr(:name).equals(self[:name])] }.to_s
-
driver.dom.xpath(other_radios_xpath).each { |node| node.remove_attribute("checked") }
-
native['checked'] = 'checked'
-
end
-
-
1
def set_checkbox(value)
-
if value && !native['checked']
-
native['checked'] = 'checked'
-
elsif !value && native['checked']
-
native.remove_attribute('checked')
-
end
-
end
-
-
1
def set_input(value)
-
if text_or_password? && attribute_is_not_blank?(:maxlength)
-
# Browser behavior for maxlength="0" is inconsistent, so we stick with
-
# Firefox, allowing no input
-
value = value.to_s[0...self[:maxlength].to_i]
-
end
-
if Array === value #Assert multiple attribute is present
-
value.each do |v|
-
new_native = native.clone
-
new_native.remove_attribute('value')
-
native.add_next_sibling(new_native)
-
new_native['value'] = v.to_s
-
end
-
native.remove
-
else
-
if self[:readonly]
-
warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
-
else
-
native['value'] = value.to_s
-
end
-
end
-
end
-
-
1
def attribute_is_not_blank?(attribute)
-
self[attribute] && !self[attribute].empty?
-
end
-
-
1
protected
-
-
1
def checkbox?
-
input_field? && type == 'checkbox'
-
end
-
-
1
def input_field?
-
tag_name == 'input'
-
end
-
-
1
def radio?
-
input_field? && type == 'radio'
-
end
-
-
1
def textarea?
-
tag_name == "textarea"
-
end
-
-
1
def text_or_password?
-
input_field? && (type == 'text' || type == 'password')
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'forwardable'
-
-
1
module Capybara
-
-
##
-
# A {Capybara::Result} represents a collection of {Capybara::Node::Element} on the page. It is possible to interact with this
-
# collection similar to an Array because it implements Enumerable and offers the following Array methods through delegation:
-
#
-
# * []
-
# * each()
-
# * at()
-
# * size()
-
# * count()
-
# * length()
-
# * first()
-
# * last()
-
# * empty?()
-
#
-
# @see Capybara::Node::Element
-
#
-
1
class Result
-
1
include Enumerable
-
1
extend Forwardable
-
-
1
def initialize(elements, query)
-
79
@elements = elements
-
79
@result_cache = []
-
157
@results_enum = lazy_select_elements { |node| query.matches_filters?(node) }
-
79
@query = query
-
# JRuby < 9.1.6.0 has an issue with eagerly finding next in lazy enumerators which
-
# causes a concurrency issue with network requests here
-
# https://github.com/jruby/jruby/issues/4212
-
# Just force all the results to be evaluated
-
79
full_results if RUBY_PLATFORM == 'java' && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.1.6.0'))
-
end
-
-
1
def_delegators :full_results, :size, :length, :last, :values_at, :inspect, :sample
-
-
1
alias :index :find_index
-
-
1
def each(&block)
-
233
return enum_for(:each) unless block_given?
-
-
233
@result_cache.each(&block)
-
77
loop do
-
77
next_result = @results_enum.next
-
76
@result_cache << next_result
-
76
block.call(next_result)
-
end
-
1
self
-
end
-
-
1
def [](*args)
-
if (args.size == 1) && ((idx = args[0]).is_a? Integer) && (idx >= 0)
-
@result_cache << @results_enum.next while @result_cache.size <= idx
-
@result_cache[idx]
-
else
-
full_results[*args]
-
end
-
rescue StopIteration
-
return nil
-
end
-
1
alias :at :[]
-
-
1
def empty?
-
155
!any?
-
end
-
-
1
def matches_count?
-
# Only check filters for as many elements as necessary to determine result
-
1
if @query.options[:count]
-
count_opt = Integer(@query.options[:count])
-
loop do
-
break if @result_cache.size > count_opt
-
@result_cache << @results_enum.next
-
end
-
return @result_cache.size == count_opt
-
end
-
-
1
if @query.options[:minimum]
-
min_opt = Integer(@query.options[:minimum])
-
begin
-
@result_cache << @results_enum.next while @result_cache.size < min_opt
-
rescue StopIteration
-
return false
-
end
-
end
-
-
1
if @query.options[:maximum]
-
max_opt = Integer(@query.options[:maximum])
-
begin
-
@result_cache << @results_enum.next while @result_cache.size <= max_opt
-
return false
-
rescue StopIteration
-
end
-
end
-
-
1
if @query.options[:between]
-
max = Integer(@query.options[:between].max)
-
loop do
-
break if @result_cache.size > max
-
@result_cache << @results_enum.next
-
end
-
return false unless (@query.options[:between] === @result_cache.size)
-
end
-
-
1
return true
-
end
-
-
1
def failure_message
-
message = @query.failure_message
-
if count > 0
-
message << ", found #{count} #{Capybara::Helpers.declension("match", "matches", count)}: " << full_results.map(&:text).map(&:inspect).join(", ")
-
else
-
message << " but there were no matches"
-
end
-
unless rest.empty?
-
elements = rest.map(&:text).map(&:inspect).join(", ")
-
message << ". Also found " << elements << ", which matched the selector but not all filters."
-
end
-
message
-
end
-
-
1
def negative_failure_message
-
failure_message.sub(/(to find)/, 'not \1')
-
end
-
-
1
private
-
-
1
def full_results
-
158
loop { @result_cache << @results_enum.next }
-
78
@result_cache
-
end
-
-
1
def rest
-
@rest ||= @elements - full_results
-
end
-
-
1
def lazy_select_elements(&block)
-
79
if @elements.respond_to? :lazy #Ruby 2.0+
-
79
@elements.lazy.select(&block)
-
else
-
Enumerator.new do |yielder|
-
@elements.each do |val|
-
yielder.yield(val) if block.call(val)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/dsl'
-
1
require 'rspec/core'
-
1
require 'capybara/rspec/matchers'
-
1
require 'capybara/rspec/features'
-
-
1
RSpec.configure do |config|
-
1
config.include Capybara::DSL, :type => :feature
-
1
config.include Capybara::RSpecMatchers, :type => :feature
-
1
config.include Capybara::RSpecMatchers, :type => :view
-
-
# A work-around to support accessing the current example that works in both
-
# RSpec 2 and RSpec 3.
-
1
fetch_current_example = RSpec.respond_to?(:current_example) ?
-
14
proc { RSpec.current_example } : proc { |context| context.example }
-
-
# The before and after blocks must run instantaneously, because Capybara
-
# might not actually be used in all examples where it's included.
-
1
config.after do
-
14
if self.class.include?(Capybara::DSL)
-
14
Capybara.reset_sessions!
-
14
Capybara.use_default_driver
-
end
-
end
-
1
config.before do
-
14
if self.class.include?(Capybara::DSL)
-
14
example = fetch_current_example.call(self)
-
14
Capybara.current_driver = Capybara.javascript_driver if example.metadata[:js]
-
14
Capybara.current_driver = example.metadata[:driver] if example.metadata[:driver]
-
end
-
end
-
end
-
-
# frozen_string_literal: true
-
1
if RSpec::Core::Version::STRING.to_f >= 3.0
-
1
RSpec.shared_context "Capybara Features", capybara_feature: true do
-
4
instance_eval do
-
4
alias background before
-
4
alias given let
-
4
alias given! let!
-
end
-
end
-
-
# ensure shared_context is included if default shared_context_metadata_behavior is changed
-
1
if RSpec::Core::Version::STRING.to_f >= 3.5
-
1
RSpec.configure do |config|
-
1
config.include_context "Capybara Features", capybara_feature: true
-
end
-
end
-
-
1
RSpec.configure do |config|
-
1
config.alias_example_group_to :feature, capybara_feature: true, type: :feature
-
1
config.alias_example_group_to :xfeature, capybara_feature: true, type: :feature, skip: "Temporarily disabled with xfeature"
-
1
config.alias_example_group_to :ffeature, capybara_feature: true, type: :feature, focus: true
-
1
config.alias_example_to :scenario
-
1
config.alias_example_to :xscenario, skip: "Temporarily disabled with xscenario"
-
1
config.alias_example_to :fscenario, focus: true
-
end
-
else
-
module Capybara
-
module Features
-
def self.included(base)
-
base.instance_eval do
-
alias :background :before
-
alias :scenario :it
-
alias :xscenario :xit
-
alias :given :let
-
alias :given! :let!
-
alias :feature :describe
-
end
-
end
-
end
-
end
-
-
def self.feature(*args, &block)
-
options = if args.last.is_a?(Hash) then args.pop else {} end
-
options[:capybara_feature] = true
-
options[:type] = :feature
-
options[:caller] ||= caller
-
args.push(options)
-
-
#call describe on RSpec in case user has expose_dsl_globally set to false
-
RSpec.describe(*args, &block)
-
end
-
-
RSpec.configure do |config|
-
config.include(Capybara::Features, capybara_feature: true)
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module RSpecMatchers
-
1
class Matcher
-
1
include ::RSpec::Matchers::Composable if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.0'))
-
-
1
attr_reader :failure_message, :failure_message_when_negated
-
-
1
def wrap(actual)
-
3
if actual.respond_to?("has_selector?")
-
3
actual
-
else
-
Capybara.string(actual.to_s)
-
end
-
end
-
-
# RSpec 2 compatibility:
-
1
def failure_message_for_should; failure_message end
-
1
def failure_message_for_should_not; failure_message_when_negated end
-
-
1
private
-
-
1
def wrap_matches?(actual)
-
2
yield(wrap(actual))
-
rescue Capybara::ExpectationNotMet => e
-
@failure_message = e.message
-
return false
-
end
-
-
1
def wrap_does_not_match?(actual)
-
1
yield(wrap(actual))
-
rescue Capybara::ExpectationNotMet => e
-
@failure_message_when_negated = e.message
-
return false
-
end
-
end
-
-
1
class HaveSelector < Matcher
-
-
1
def initialize(*args, &filter_block)
-
1
@args = args
-
1
@filter_block = filter_block
-
end
-
-
1
def matches?(actual)
-
wrap_matches?(actual){ |el| el.assert_selector(*@args, &@filter_block) }
-
end
-
-
1
def does_not_match?(actual)
-
2
wrap_does_not_match?(actual){ |el| el.assert_no_selector(*@args, &@filter_block) }
-
end
-
-
1
def description
-
"have #{query.description}"
-
end
-
-
1
def query
-
@query ||= Capybara::Queries::SelectorQuery.new(*@args, &@filter_block)
-
end
-
end
-
-
1
class MatchSelector < HaveSelector
-
1
def matches?(actual)
-
wrap_matches?(actual) { |el| el.assert_matches_selector(*@args, &@filter_block) }
-
end
-
-
1
def does_not_match?(actual)
-
wrap_does_not_match?(actual) { |el| el.assert_not_matches_selector(*@args, &@filter_block) }
-
end
-
-
1
def description
-
"match #{query.description}"
-
end
-
-
1
def query
-
@query ||= Capybara::Queries::MatchQuery.new(*@args)
-
end
-
end
-
-
1
class HaveText < Matcher
-
1
attr_reader :type, :content, :options
-
-
1
def initialize(*args)
-
2
@args = args.dup
-
-
# are set just for backwards compatability
-
2
@type = args.shift if args.first.is_a?(Symbol)
-
2
@content = args.shift
-
2
@options = (args.first.is_a?(Hash))? args.first : {}
-
end
-
-
1
def matches?(actual)
-
4
wrap_matches?(actual) { |el| el.assert_text(*@args) }
-
end
-
-
1
def does_not_match?(actual)
-
wrap_does_not_match?(actual) { |el| el.assert_no_text(*@args) }
-
end
-
-
1
def description
-
"text #{format(content)}"
-
end
-
-
1
def format(content)
-
content = Capybara::Helpers.normalize_whitespace(content) unless content.is_a? Regexp
-
content.inspect
-
end
-
end
-
-
1
class HaveTitle < Matcher
-
1
attr_reader :title
-
-
1
def initialize(*args)
-
@args = args
-
-
# are set just for backwards compatability
-
@title = args.first
-
end
-
-
1
def matches?(actual)
-
wrap_matches?(actual) { |el| el.assert_title(*@args) }
-
end
-
-
1
def does_not_match?(actual)
-
wrap_does_not_match?(actual) { |el| el.assert_no_title(*@args) }
-
end
-
-
1
def description
-
"have title #{title.inspect}"
-
end
-
end
-
-
1
class HaveCurrentPath < Matcher
-
1
attr_reader :current_path
-
-
1
def initialize(*args)
-
@args = args
-
-
# are set just for backwards compatability
-
@current_path = args.first
-
end
-
-
1
def matches?(actual)
-
wrap_matches?(actual) { |el| el.assert_current_path(*@args) }
-
end
-
-
1
def does_not_match?(actual)
-
wrap_does_not_match?(actual) { |el| el.assert_no_current_path(*@args) }
-
end
-
-
1
def description
-
"have current path #{current_path.inspect}"
-
end
-
end
-
-
1
class BecomeClosed
-
1
def initialize(options)
-
@wait_time = Capybara::Queries::BaseQuery.wait(options)
-
end
-
-
1
def matches?(window)
-
@window = window
-
start_time = Capybara::Helpers.monotonic_time
-
while window.exists?
-
return false if (Capybara::Helpers.monotonic_time - start_time) > @wait_time
-
sleep 0.05
-
end
-
true
-
end
-
-
1
def failure_message
-
"expected #{@window.inspect} to become closed after #{@wait_time} seconds"
-
end
-
-
1
def failure_message_when_negated
-
"expected #{@window.inspect} not to become closed after #{@wait_time} seconds"
-
end
-
-
# RSpec 2 compatibility:
-
1
alias_method :failure_message_for_should, :failure_message
-
1
alias_method :failure_message_for_should_not, :failure_message_when_negated
-
end
-
-
1
def have_selector(*args, &optional_filter_block)
-
HaveSelector.new(*args, &optional_filter_block)
-
end
-
-
1
def match_selector(*args, &optional_filter_block)
-
MatchSelector.new(*args, &optional_filter_block)
-
end
-
# defined_negated_matcher was added in RSpec 3.1 - it's syntactic sugar only since a user can do
-
# expect(page).not_to match_selector, so not sure we really need to support not_match_selector for prior to RSpec 3.1
-
1
::RSpec::Matchers.define_negated_matcher :not_match_selector, :match_selector if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.1'))
-
-
-
1
def have_xpath(xpath, options={}, &optional_filter_block)
-
HaveSelector.new(:xpath, xpath, options, &optional_filter_block)
-
end
-
-
1
def match_xpath(xpath, options={}, &optional_filter_block)
-
MatchSelector.new(:xpath, xpath, options, &optional_filter_block)
-
end
-
-
1
def have_css(css, options={}, &optional_filter_block)
-
HaveSelector.new(:css, css, options, &optional_filter_block)
-
end
-
-
1
def match_css(css, options={}, &optional_filter_block)
-
MatchSelector.new(:css, css, options, &optional_filter_block)
-
end
-
-
1
def have_text(*args)
-
2
HaveText.new(*args)
-
end
-
1
alias_method :have_content, :have_text
-
-
1
def have_title(title, options = {})
-
HaveTitle.new(title, options)
-
end
-
-
1
def have_current_path(path, options = {})
-
HaveCurrentPath.new(path, options)
-
end
-
-
1
def have_link(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:link, locator, options, &optional_filter_block)
-
end
-
-
1
def have_button(locator=nil, options={}, &optional_filter_block)
-
1
locator, options = nil, locator if locator.is_a? Hash
-
1
HaveSelector.new(:button, locator, options, &optional_filter_block)
-
end
-
-
1
def have_field(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:field, locator, options, &optional_filter_block)
-
end
-
-
1
def have_checked_field(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:field, locator, options.merge(checked: true), &optional_filter_block)
-
end
-
-
1
def have_unchecked_field(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:field, locator, options.merge(unchecked: true), &optional_filter_block)
-
end
-
-
1
def have_select(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:select, locator, options, &optional_filter_block)
-
end
-
-
1
def have_table(locator=nil, options={}, &optional_filter_block)
-
locator, options = nil, locator if locator.is_a? Hash
-
HaveSelector.new(:table, locator, options, &optional_filter_block)
-
end
-
-
##
-
# Wait for window to become closed.
-
# @example
-
# expect(window).to become_closed(wait: 0.8)
-
# @param options [Hash] optional param
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum wait time
-
1
def become_closed(options = {})
-
BecomeClosed.new(options)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/selector/selector'
-
1
Capybara::Selector::FilterSet.add(:_field) do
-
1
filter(:checked, :boolean) { |node, value| not(value ^ node.checked?) }
-
1
filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
-
62
filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
-
1
filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
-
-
1
describe do |options|
-
desc, states = String.new, []
-
states << 'checked' if options[:checked] || (options[:unchecked] === false)
-
states << 'not checked' if options[:unchecked] || (options[:checked] === false)
-
states << 'disabled' if options[:disabled] == true
-
desc << " that is #{states.join(' and ')}" unless states.empty?
-
desc << " with the multiple attribute" if options[:multiple] == true
-
desc << " without the multiple attribute" if options[:multiple] === false
-
desc
-
end
-
end
-
-
##
-
#
-
# Select elements by XPath expression
-
#
-
# @locator An XPath expression
-
#
-
1
Capybara.add_selector(:xpath) do
-
3
xpath { |xpath| xpath }
-
end
-
-
##
-
#
-
# Select elements by CSS selector
-
#
-
# @locator A CSS selector
-
#
-
1
Capybara.add_selector(:css) do
-
1
css { |css| css }
-
end
-
-
##
-
#
-
# Select element by id
-
#
-
# @locator The id of the element to match
-
#
-
1
Capybara.add_selector(:id) do
-
1
xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
-
end
-
-
##
-
#
-
# Select field elements (input [not of type submit, image, or hidden], textarea, select)
-
#
-
# @locator Matches against the id, name, or placeholder
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String] :placeholder Matches the placeholder attribute
-
# @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select'
-
# @filter [Boolean] :readonly
-
# @filter [String] :with Matches the current value of the field
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :checked Match checked fields?
-
# @filter [Boolean] :unchecked Match unchecked fields?
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [Boolean] :multiple Match fields that accept multiple values
-
1
Capybara.add_selector(:field) do
-
1
xpath(:name, :placeholder, :type) do |locator, options|
-
xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
-
if options[:type]
-
type=options[:type].to_s
-
if ['textarea', 'select'].include?(type)
-
xpath = XPath.descendant(type.to_sym)
-
else
-
xpath = xpath[XPath.attr(:type).equals(type)]
-
end
-
end
-
xpath=locate_field(xpath, locator, options)
-
xpath
-
end
-
-
1
filter_set(:_field) # checked/unchecked/disabled/multiple
-
-
1
filter(:readonly, :boolean) { |node, value| not(value ^ node.readonly?) }
-
1
filter(:with) do |node, with|
-
with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
-
end
-
1
describe do |options|
-
desc = String.new
-
(expression_filters - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.has_key?(ef) }
-
desc << " of type #{options[:type].inspect}" if options[:type]
-
desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
-
desc
-
end
-
end
-
-
##
-
#
-
# Select fieldset elements
-
#
-
# @locator Matches id or contents of wrapped legend
-
#
-
# @filter [String] :id Matches id attribute
-
# @filter [String] :legend Matches contents of wrapped legend
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
#
-
1
Capybara.add_selector(:fieldset) do
-
1
xpath(:legend) do |locator, options|
-
xpath = XPath.descendant(:fieldset)
-
xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil?
-
xpath = xpath[XPath.child(:legend)[XPath.string.n.is(options[:legend])]] if options[:legend]
-
xpath
-
end
-
end
-
-
##
-
#
-
# Find links ( <a> elements with an href attribute )
-
#
-
# @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
-
#
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :title Matches the title attribute
-
# @filter [String] :alt Matches the alt attribute of a contained img element
-
# @filter [String] :class Matches the class(es) provided
-
# @filter [String, Regexp,nil] :href Matches the normalized href of the link, if nil will find <a> elements with no href attribute
-
#
-
1
Capybara.add_selector(:link) do
-
1
xpath(:title, :alt) do |locator, options={}|
-
xpath = XPath.descendant(:a)
-
xpath = if options.fetch(:href, true).nil?
-
xpath[~XPath.attr(:href)]
-
else
-
xpath[XPath.attr(:href)]
-
end
-
unless locator.nil?
-
locator = locator.to_s
-
matchers = XPath.attr(:id).equals(locator) |
-
XPath.string.n.is(locator) |
-
XPath.attr(:title).is(locator) |
-
XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
-
matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
-
xpath = xpath[matchers]
-
end
-
xpath = [:title].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
-
xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt).equals(options[:alt])]] if options[:alt]
-
xpath
-
end
-
-
1
filter(:href) do |node, href|
-
case href
-
when nil
-
true
-
when Regexp
-
node[:href].match href
-
else
-
node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)], minimum: 0)
-
end
-
end
-
-
1
describe do |options|
-
desc = String.new()
-
desc << " with href #{options[:href].inspect}" if options[:href]
-
desc << " with no href attribute" if options.fetch(:href, true).nil?
-
end
-
end
-
-
##
-
#
-
# Find buttons ( input [of type submit, reset, image, button] or button elements )
-
#
-
# @locator Matches the id, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
-
#
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :title Matches the title attribute
-
# @filter [String] :class Matches the class(es) provided
-
# @filter [String] :value Matches the value of an input button
-
#
-
1
Capybara.add_selector(:button) do
-
1
xpath(:value, :title) do |locator, options={}|
-
16
input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
-
16
btn_xpath = XPath.descendant(:button)
-
16
image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')]
-
-
16
unless locator.nil?
-
16
locator = locator.to_s
-
16
locator_matches = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
-
16
locator_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
-
-
16
input_btn_xpath = input_btn_xpath[locator_matches]
-
-
16
btn_xpath = btn_xpath[locator_matches | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
-
-
16
alt_matches = XPath.attr(:alt).is(locator)
-
16
alt_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
-
16
image_btn_xpath = image_btn_xpath[alt_matches]
-
end
-
-
16
res_xpath = input_btn_xpath + btn_xpath + image_btn_xpath
-
-
48
res_xpath = expression_filters.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
-
-
16
res_xpath
-
end
-
-
16
filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
-
-
1
describe do |options|
-
desc = String.new
-
desc << " that is disabled" if options[:disabled] == true
-
desc << describe_all_expression_filters(options)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find links or buttons
-
#
-
1
Capybara.add_selector(:link_or_button) do
-
1
label "link or button"
-
1
xpath do |locator, options|
-
self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator, options)}.reduce(:+)
-
end
-
-
1
filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
-
-
1
describe { |options| " that is disabled" if options[:disabled] }
-
end
-
-
##
-
#
-
# Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
-
#
-
# @locator Matches against the id, name, or placeholder
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String] :placeholder Matches the placeholder attribute
-
# @filter [String] :with Matches the current value of the field
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [Boolean] :multiple Match fields that accept multiple values
-
#
-
1
Capybara.add_selector(:fillable_field) do
-
1
label "field"
-
1
xpath(:name, :placeholder) do |locator, options|
-
61
xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
-
61
locate_field(xpath, locator, options)
-
end
-
-
1
filter_set(:_field, [:disabled, :multiple])
-
-
1
filter(:with) do |node, with|
-
with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
-
end
-
-
1
describe do |options|
-
desc = String.new
-
desc << describe_all_expression_filters(options)
-
desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find radio buttons
-
#
-
# @locator Match id, name, or associated label text
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :checked Match checked fields?
-
# @filter [Boolean] :unchecked Match unchecked fields?
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [String] :option Match the value
-
#
-
1
Capybara.add_selector(:radio_button) do
-
1
label "radio button"
-
1
xpath(:name) do |locator, options|
-
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
-
locate_field(xpath, locator, options)
-
end
-
-
1
filter_set(:_field, [:checked, :unchecked, :disabled])
-
-
1
filter(:option) { |node, value| node.value == value.to_s }
-
-
1
describe do |options|
-
desc = String.new
-
desc << " with value #{options[:option].inspect}" if options[:option]
-
desc << describe_all_expression_filters(options)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find checkboxes
-
#
-
# @locator Match id, name, or associated label text
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :checked Match checked fields?
-
# @filter [Boolean] :unchecked Match unchecked fields?
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [String] :option Match the value
-
#
-
1
Capybara.add_selector(:checkbox) do
-
1
xpath(:name) do |locator, options|
-
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
-
locate_field(xpath, locator, options)
-
end
-
-
1
filter_set(:_field, [:checked, :unchecked, :disabled])
-
-
1
filter(:option) { |node, value| node.value == value.to_s }
-
-
1
describe do |options|
-
desc = String.new
-
desc << " with value #{options[:option].inspect}" if options[:option]
-
desc << describe_all_expression_filters(options)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find select elements
-
#
-
# @locator Match id, name, placeholder, or associated label text
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String] :placeholder Matches the placeholder attribute
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [Boolean] :multiple Match fields that accept multiple values
-
# @filter [Array<String>] :options Exact match options
-
# @filter [Array<String>] :with_options Partial match options
-
# @filter [String, Array<String>] :selected Match the selection(s)
-
#
-
1
Capybara.add_selector(:select) do
-
1
label "select box"
-
1
xpath(:name, :placeholder) do |locator, options|
-
xpath = XPath.descendant(:select)
-
locate_field(xpath, locator, options)
-
end
-
-
1
filter_set(:_field, [:disabled, :multiple])
-
-
1
filter(:options) do |node, options|
-
if node.visible?
-
actual = node.all(:xpath, './/option').map { |option| option.text }
-
else
-
actual = node.all(:xpath, './/option', visible: false).map { |option| option.text(:all) }
-
end
-
options.sort == actual.sort
-
end
-
-
1
filter(:with_options) do |node, options|
-
finder_settings = { minimum: 0 }
-
if !node.visible?
-
finder_settings[:visible] = false
-
end
-
options.all? { |option| node.first(:option, option, finder_settings) }
-
end
-
-
1
filter(:selected) do |node, selected|
-
actual = node.all(:xpath, './/option', visible: false).select { |option| option.selected? }.map { |option| option.text(:all) }
-
[selected].flatten.sort == actual.sort
-
end
-
-
1
describe do |options|
-
desc = String.new
-
desc << " with options #{options[:options].inspect}" if options[:options]
-
desc << " with at least options #{options[:with_options].inspect}" if options[:with_options]
-
desc << " with #{options[:selected].inspect} selected" if options[:selected]
-
desc << describe_all_expression_filters(options)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find option elements
-
#
-
# @locator Match text of option
-
# @filter [Boolean] :disabled Match disabled option
-
# @filter [Boolean] :selected Match selected option
-
#
-
1
Capybara.add_selector(:option) do
-
1
xpath do |locator|
-
xpath = XPath.descendant(:option)
-
xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
-
xpath
-
end
-
-
1
filter(:disabled, :boolean) { |node, value| not(value ^ node.disabled?) }
-
1
filter(:selected, :boolean) { |node, value| not(value ^ node.selected?) }
-
-
1
describe do |options|
-
desc = String.new
-
desc << " that is#{' not' unless options[:disabled]} disabled" if options.has_key?(:disabled)
-
desc << " that is#{' not' unless options[:selected]} selected" if options.has_key?(:selected)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find file input elements
-
#
-
# @locator Match id, name, or associated label text
-
# @filter [String] :id Matches the id attribute
-
# @filter [String] :name Matches the name attribute
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
# @filter [Boolean] :disabled Match disabled field?
-
# @filter [Boolean] :multiple Match field that accepts multiple values
-
#
-
1
Capybara.add_selector(:file_field) do
-
1
label "file field"
-
1
xpath(:name) do |locator, options|
-
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
-
locate_field(xpath, locator, options)
-
end
-
-
1
filter_set(:_field, [:disabled, :multiple])
-
-
1
describe do |options|
-
desc = String.new
-
desc << describe_all_expression_filters(options)
-
desc
-
end
-
end
-
-
##
-
#
-
# Find label elements
-
#
-
# @locator Match id or text contents
-
# @filter [Element, String] :for The element or id of the element associated with the label
-
#
-
1
Capybara.add_selector(:label) do
-
1
label "label"
-
1
xpath(:for) do |locator, options|
-
xpath = XPath.descendant(:label)
-
xpath = xpath[XPath.string.n.is(locator.to_s) | XPath.attr(:id).equals(locator.to_s)] unless locator.nil?
-
if options.has_key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
-
xpath = xpath[XPath.attr(:for).equals(options[:for].to_s).or((~XPath.attr(:for)).and(XPath.descendant()[XPath.attr(:id).equals(options[:for].to_s)]))]
-
end
-
xpath
-
end
-
-
1
filter(:for) do |node, field_or_value|
-
if field_or_value.is_a? Capybara::Node::Element
-
if node[:for]
-
field_or_value[:id] == node[:for]
-
else
-
field_or_value.find_xpath('./ancestor::label[1]').include? node.base
-
end
-
else
-
#Non element values were handled through the expression filter
-
true
-
end
-
end
-
-
1
describe do |options|
-
desc = String.new
-
desc << " for #{options[:for]}" if options[:for]
-
desc
-
end
-
end
-
-
##
-
#
-
# Find table elements
-
#
-
# @locator id or caption text of table
-
# @filter [String] :id Match id attribute of table
-
# @filter [String] :caption Match text of associated caption
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
#
-
1
Capybara.add_selector(:table) do
-
1
xpath(:caption) do |locator, options|
-
xpath = XPath.descendant(:table)
-
xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil?
-
xpath = xpath[XPath.descendant(:caption).equals(options[:caption])] if options[:caption]
-
xpath
-
end
-
-
1
describe do |options|
-
desc = String.new
-
desc << " with caption #{options[:caption]}" if options[:caption]
-
desc
-
end
-
end
-
-
##
-
#
-
# Find frame/iframe elements
-
#
-
# @locator Match id or name
-
# @filter [String] :id Match id attribute
-
# @filter [String] :name Match name attribute
-
# @filter [String, Array<String>] :class Matches the class(es) provided
-
#
-
1
Capybara.add_selector(:frame) do
-
1
xpath(:name) do |locator, options|
-
xpath = XPath.descendant(:iframe) + XPath.descendant(:frame)
-
xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.attr(:name).equals(locator)] unless locator.nil?
-
xpath = expression_filters.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
-
xpath
-
end
-
-
1
describe do |options|
-
desc = String.new
-
desc << " with name #{options[:name]}" if options[:name]
-
desc
-
end
-
end
-
1
module Capybara
-
1
class Selector
-
1
class CSS
-
1
def self.escape(str)
-
out = String.new("")
-
value = str.dup
-
out << value.slice!(0...1) if value =~ /^[-_]/
-
out << if value[0] =~ NMSTART
-
value.slice!(0...1)
-
else
-
escape_char(value.slice!(0...1))
-
end
-
out << value.gsub(/[^a-zA-Z0-9_-]/) {|c| escape_char c}
-
out
-
end
-
-
1
def self.escape_char(c)
-
return "\\%06x" % c.ord() unless c =~ %r{[ -/:-~]}
-
"\\#{c}"
-
end
-
-
1
S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
-
1
H = /[0-9a-fA-F]/
-
1
UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
-
1
NONASCII = /[#{S}]/
-
1
ESCAPE = /#{UNICODE}|\\[ -~#{S}]/
-
1
NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
class Selector
-
1
class Filter
-
1
def initialize(name, block, options={})
-
18
@name = name
-
18
@block = block
-
18
@options = options
-
18
@options[:valid_values] = [true,false] if options[:boolean]
-
end
-
-
1
def default?
-
198
@options.has_key?(:default)
-
end
-
-
1
def default
-
76
@options[:default]
-
end
-
-
1
def matches?(node, value)
-
76
return true if skip?(value)
-
-
76
if !valid_value?(value)
-
msg = "Invalid value #{value.inspect} passed to filter #{@name} - "
-
if default?
-
warn msg + "defaulting to #{default}"
-
value = default
-
else
-
warn msg + "skipping"
-
return true
-
end
-
end
-
-
76
@block.call(node, value)
-
end
-
-
1
def skip?(value)
-
76
@options.has_key?(:skip_if) && value == @options[:skip_if]
-
end
-
-
1
private
-
-
1
def valid_value?(value)
-
76
!@options.has_key?(:valid_values) || Array(@options[:valid_values]).include?(value)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/selector/filter'
-
-
1
module Capybara
-
1
class Selector
-
1
class FilterSet
-
1
attr_reader :descriptions
-
-
1
def initialize(name, &block)
-
18
@name = name
-
18
@descriptions = []
-
18
instance_eval(&block)
-
end
-
-
1
def filter(name, *types_and_options, &block)
-
4
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
-
8
types_and_options.each { |k| options[k] = true}
-
4
filters[name] = Filter.new(name, block, options)
-
end
-
-
1
def describe(&block)
-
20
descriptions.push block
-
end
-
-
1
def description(options={})
-
@descriptions.map {|desc| desc.call(options).to_s }.join
-
end
-
-
1
def filters
-
197
@filters ||= {}
-
end
-
-
1
class << self
-
1
def all
-
24
@filter_sets ||= {}
-
end
-
-
1
def add(name, &block)
-
18
all[name.to_sym] = FilterSet.new(name.to_sym, &block)
-
end
-
-
1
def remove(name)
-
all.delete(name.to_sym)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/selector/filter_set'
-
1
require 'capybara/selector/css'
-
1
require 'xpath'
-
-
#Patch XPath to allow a nil condition in where
-
1
module XPath
-
1
class Renderer
-
1
undef :where if method_defined?(:where)
-
1
def where(on, condition)
-
805
condition = condition.to_s
-
805
if !condition.empty?
-
433
"#{on}[#{condition}]"
-
else
-
372
"#{on}"
-
end
-
end
-
end
-
end
-
-
1
module Capybara
-
1
class Selector
-
-
1
attr_reader :name, :format, :expression_filters
-
-
1
class << self
-
1
def all
-
96
@selectors ||= {}
-
end
-
-
1
def add(name, &block)
-
17
all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
-
end
-
-
1
def update(name, &block)
-
all[name.to_sym].instance_eval(&block)
-
end
-
-
1
def remove(name)
-
all.delete(name.to_sym)
-
end
-
end
-
-
1
def initialize(name, &block)
-
17
@name = name
-
17
@filter_set = FilterSet.add(name){}
-
17
@match = nil
-
17
@label = nil
-
17
@failure_message = nil
-
17
@description = nil
-
17
@format = nil
-
17
@expression = nil
-
17
@expression_filters = []
-
17
@default_visibility = nil
-
17
instance_eval(&block)
-
end
-
-
1
def custom_filters
-
187
@filter_set.filters
-
end
-
-
##
-
#
-
# Define a selector by an xpath expression
-
#
-
# @overload xpath(*expression_filters, &block)
-
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this expression
-
# @yield [locator, options] The block to use to generate the XPath expression
-
# @yieldparam [String] locator The locator string passed to the query
-
# @yieldparam [Hash] options The options hash passed to the query
-
# @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
-
#
-
# @overload xpath()
-
# @return [#call] The block that will be called to generate the XPath expression
-
#
-
1
def xpath(*expression_filters, &block)
-
16
@format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
-
16
format == :xpath ? @expression : nil
-
end
-
-
##
-
#
-
# Define a selector by a CSS selector
-
#
-
# @overload css(*expression_filters, &block)
-
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
-
# @yield [locator, options] The block to use to generate the CSS selector
-
# @yieldparam [String] locator The locator string passed to the query
-
# @yieldparam [Hash] options The options hash passed to the query
-
# @yieldreturn [#to_s] An object that can produce a CSS selector
-
#
-
# @overload css()
-
# @return [#call] The block that will be called to generate the CSS selector
-
#
-
1
def css(*expression_filters, &block)
-
1
@format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
-
1
format == :css ? @expression : nil
-
end
-
-
##
-
#
-
# Automatic selector detection
-
#
-
# @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
-
# @yieldparam [String], locator The locator string used to determin if it matches the selector
-
# @yieldreturn [Boolean] Whether this selector matches the locator string
-
# @return [#call] The block that will be used to detect selector match
-
#
-
1
def match(&block)
-
@match = block if block
-
@match
-
end
-
-
##
-
#
-
# Set/get a descriptive label for the selector
-
#
-
# @overload label(label)
-
# @param [String] label A descriptive label for this selector - used in error messages
-
# @overload label()
-
# @return [String] The currently set label
-
#
-
1
def label(label=nil)
-
6
@label = label if label
-
6
@label
-
end
-
-
##
-
#
-
# Description of the selector
-
#
-
# @param [Hash] options The options of the query used to generate the description
-
# @return [String] Description of the selector when used with the options passed
-
#
-
1
def description(options={})
-
@filter_set.description(options)
-
end
-
-
1
def call(locator, options={})
-
79
if format
-
# @expression.call(locator, options.select {|k,v| @expression_filters.include?(k)})
-
79
@expression.call(locator, options)
-
else
-
warn "Selector has no format"
-
end
-
end
-
-
##
-
#
-
# Should this selector be used for the passed in locator
-
#
-
# This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
-
#
-
# @param [String] locator The locator passed to the query
-
# @return [Boolean] Whether or not to use this selector
-
#
-
1
def match?(locator)
-
@match and @match.call(locator)
-
end
-
-
##
-
#
-
# Define a non-expression filter for use with this selector
-
#
-
# @overload filter(name, *types, options={}, &block)
-
# @param [Symbol] name The filter name
-
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
-
# @param [Hash] options ({}) Options of the filter
-
# @option options [Array<>] :valid_values Valid values for this filter
-
# @option options :default The default value of the filter (if any)
-
# @option options :skip_if Value of the filter that will cause it to be skipped
-
#
-
1
def filter(name, *types_and_options, &block)
-
14
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
-
19
types_and_options.each { |k| options[k] = true}
-
14
custom_filters[name] = Filter.new(name, block, options)
-
end
-
-
1
def filter_set(name, filters_to_use = nil)
-
6
f_set = FilterSet.all[name]
-
6
f_set.filters.each do |n, filter|
-
24
custom_filters[n] = filter if filters_to_use.nil? || filters_to_use.include?(n)
-
end
-
12
f_set.descriptions.each { |desc| @filter_set.describe(&desc) }
-
end
-
-
1
def describe &block
-
13
@filter_set.describe(&block)
-
end
-
-
##
-
#
-
# Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
-
# If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
-
#
-
# @param [Symbol] default_visibility Only find elements with the specified visibility:
-
# * :all - finds visible and invisible elements.
-
# * :hidden - only finds invisible elements.
-
# * :visible - only finds visible elements.
-
1
def visible(default_visibility)
-
@default_visibility = default_visibility
-
end
-
-
1
def default_visibility
-
78
if @default_visibility.nil?
-
78
Capybara.ignore_hidden_elements
-
else
-
@default_visibility
-
end
-
end
-
-
1
private
-
-
1
def locate_field(xpath, locator, options={})
-
61
locate_xpath = xpath #need to save original xpath for the label wrap
-
61
if locator
-
61
locator = locator.to_s
-
61
attr_matchers = XPath.attr(:id).equals(locator) |
-
XPath.attr(:name).equals(locator) |
-
XPath.attr(:placeholder).equals(locator) |
-
XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
-
61
attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
-
-
61
locate_xpath = locate_xpath[attr_matchers]
-
61
locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
-
end
-
-
183
locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
-
61
locate_xpath
-
end
-
-
1
def describe_all_expression_filters(opts={})
-
expression_filters.map { |ef| " with #{ef} #{opts[ef]}" if opts.has_key?(ef) }.join
-
end
-
-
1
def find_by_attr(attribute, value)
-
154
finder_name = "find_by_#{attribute}_attr"
-
154
if respond_to?(finder_name, true)
-
send(finder_name, value)
-
else
-
154
value ? XPath.attr(attribute).equals(value) : nil
-
end
-
end
-
-
1
def find_by_class_attr(classes)
-
if classes
-
Array(classes).map do |klass|
-
"contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
-
end.join(" and ").to_sym
-
else
-
nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require "uri"
-
-
1
class Capybara::Selenium::Driver < Capybara::Driver::Base
-
-
1
DEFAULT_OPTIONS = {
-
:browser => :firefox,
-
clear_local_storage: false,
-
clear_session_storage: false
-
}
-
1
SPECIAL_OPTIONS = [:browser, :clear_local_storage, :clear_session_storage]
-
-
1
attr_reader :app, :options
-
-
1
def browser
-
unless @browser
-
if options[:browser].to_s == "firefox"
-
options[:desired_capabilities] ||= Selenium::WebDriver::Remote::Capabilities.firefox
-
options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
-
end
-
-
@browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) })
-
-
main = Process.pid
-
at_exit do
-
# Store the exit status of the test run since it goes away after calling the at_exit proc...
-
@exit_status = $!.status if $!.is_a?(SystemExit)
-
quit if Process.pid == main
-
exit @exit_status if @exit_status # Force exit with stored status
-
end
-
end
-
@browser
-
end
-
-
1
def initialize(app, options={})
-
begin
-
require 'selenium-webdriver'
-
rescue LoadError => e
-
if e.message =~ /selenium-webdriver/
-
raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
-
else
-
raise e
-
end
-
end
-
-
-
@app = app
-
@browser = nil
-
@exit_status = nil
-
@frame_handles = {}
-
@options = DEFAULT_OPTIONS.merge(options)
-
end
-
-
1
def visit(path)
-
browser.navigate.to(path)
-
end
-
-
1
def go_back
-
browser.navigate.back
-
end
-
-
1
def go_forward
-
browser.navigate.forward
-
end
-
-
1
def html
-
browser.page_source
-
end
-
-
1
def title
-
browser.title
-
end
-
-
1
def current_url
-
browser.current_url
-
end
-
-
1
def find_xpath(selector)
-
browser.find_elements(:xpath, selector).map { |node| Capybara::Selenium::Node.new(self, node) }
-
end
-
-
1
def find_css(selector)
-
browser.find_elements(:css, selector).map { |node| Capybara::Selenium::Node.new(self, node) }
-
end
-
-
1
def wait?; true; end
-
1
def needs_server?; true; end
-
-
1
def execute_script(script, *args)
-
browser.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
-
end
-
-
1
def evaluate_script(script, *args)
-
browser.execute_script("return #{script}", *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
-
end
-
-
1
def save_screenshot(path, _options={})
-
browser.save_screenshot(path)
-
end
-
-
1
def reset!
-
# Use instance variable directly so we avoid starting the browser just to reset the session
-
if @browser
-
navigated = false
-
start_time = Capybara::Helpers.monotonic_time
-
begin
-
if !navigated
-
# Only trigger a navigation if we haven't done it already, otherwise it
-
# can trigger an endless series of unload modals
-
begin
-
@browser.manage.delete_all_cookies
-
if options[:clear_session_storage]
-
if @browser.respond_to? :session_storage
-
@browser.session_storage.clear
-
else
-
warn "sessionStorage clear requested but is not available for this driver"
-
end
-
end
-
if options[:clear_local_storage]
-
if @browser.respond_to? :local_storage
-
@browser.local_storage.clear
-
else
-
warn "localStorage clear requested but is not available for this driver"
-
end
-
end
-
rescue Selenium::WebDriver::Error::UnhandledError
-
# delete_all_cookies fails when we've previously gone
-
# to about:blank, so we rescue this error and do nothing
-
# instead.
-
end
-
@browser.navigate.to("about:blank")
-
end
-
navigated = true
-
-
#Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
-
until find_xpath("/html/body/*").empty? do
-
raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
-
sleep 0.05
-
end
-
rescue Selenium::WebDriver::Error::UnhandledAlertError
-
# This error is thrown if an unhandled alert is on the page
-
# Firefox appears to automatically dismiss this alert, chrome does not
-
# We'll try to accept it
-
begin
-
@browser.switch_to.alert.accept
-
sleep 0.25 # allow time for the modal to be handled
-
rescue Selenium::WebDriver::Error::NoAlertPresentError
-
# The alert is now gone - nothing to do
-
end
-
# try cleaning up the browser again
-
retry
-
end
-
end
-
end
-
-
1
def switch_to_frame(frame)
-
case frame
-
when :top
-
@frame_handles[browser.window_handle] = []
-
browser.switch_to.default_content
-
when :parent
-
# would love to use browser.switch_to.parent_frame here
-
# but it has an issue if the current frame is removed from within it
-
@frame_handles[browser.window_handle].pop
-
browser.switch_to.default_content
-
@frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
-
else
-
@frame_handles[browser.window_handle] ||= []
-
@frame_handles[browser.window_handle] << frame.native
-
browser.switch_to.frame(frame.native)
-
end
-
end
-
-
1
def current_window_handle
-
browser.window_handle
-
end
-
-
1
def window_size(handle)
-
within_given_window(handle) do
-
size = browser.manage.window.size
-
[size.width, size.height]
-
end
-
end
-
-
1
def resize_window_to(handle, width, height)
-
within_given_window(handle) do
-
browser.manage.window.resize_to(width, height)
-
end
-
end
-
-
1
def maximize_window(handle)
-
within_given_window(handle) do
-
browser.manage.window.maximize
-
end
-
sleep 0.1 # work around for https://code.google.com/p/selenium/issues/detail?id=7405
-
end
-
-
1
def close_window(handle)
-
within_given_window(handle) do
-
browser.close
-
end
-
end
-
-
1
def window_handles
-
browser.window_handles
-
end
-
-
1
def open_new_window
-
browser.execute_script('window.open();')
-
end
-
-
1
def switch_to_window(handle)
-
browser.switch_to.window handle
-
end
-
-
1
def within_window(locator)
-
handle = find_window(locator)
-
browser.switch_to.window(handle) { yield }
-
end
-
-
1
def accept_modal(_type, options={})
-
yield if block_given?
-
modal = find_modal(options)
-
modal.send_keys options[:with] if options[:with]
-
message = modal.text
-
modal.accept
-
message
-
end
-
-
1
def dismiss_modal(_type, options={})
-
yield if block_given?
-
modal = find_modal(options)
-
message = modal.text
-
modal.dismiss
-
message
-
end
-
-
1
def quit
-
@browser.quit if @browser
-
rescue Errno::ECONNREFUSED
-
# Browser must have already gone
-
rescue Selenium::WebDriver::Error::UnknownError => e
-
unless silenced_unknown_error_message?(e.message) # Most likely already gone
-
# probably already gone but not sure - so warn
-
warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
-
end
-
ensure
-
@browser = nil
-
end
-
-
1
def invalid_element_errors
-
[Selenium::WebDriver::Error::StaleElementReferenceError,
-
Selenium::WebDriver::Error::UnhandledError,
-
Selenium::WebDriver::Error::ElementNotVisibleError,
-
Selenium::WebDriver::Error::InvalidSelectorError] # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
-
end
-
-
1
def no_such_window_error
-
Selenium::WebDriver::Error::NoSuchWindowError
-
end
-
-
# @api private
-
1
def find_window(locator)
-
handles = browser.window_handles
-
return locator if handles.include? locator
-
-
original_handle = browser.window_handle
-
handles.each do |handle|
-
switch_to_window(handle)
-
if (locator == browser.execute_script("return window.name") ||
-
browser.title.include?(locator) ||
-
browser.current_url.include?(locator))
-
switch_to_window(original_handle)
-
return handle
-
end
-
end
-
raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
-
end
-
-
#@api private
-
1
def marionette?
-
(options[:browser].to_s == "firefox") && browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)
-
end
-
-
# @deprecated This method is being removed
-
1
def browser_initialized?
-
super && !@browser.nil?
-
end
-
-
1
private
-
-
1
def within_given_window(handle)
-
original_handle = self.current_window_handle
-
if handle == original_handle
-
yield
-
else
-
switch_to_window(handle)
-
result = yield
-
switch_to_window(original_handle)
-
result
-
end
-
end
-
-
1
def find_modal(options={})
-
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
-
# Actual wait time may be longer than specified
-
wait = Selenium::WebDriver::Wait.new(
-
timeout: (options[:wait] || Capybara.default_max_wait_time),
-
ignore: Selenium::WebDriver::Error::NoAlertPresentError)
-
begin
-
wait.until do
-
alert = @browser.switch_to.alert
-
regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
-
alert.text.match(regexp) ? alert : nil
-
end
-
rescue Selenium::WebDriver::Error::TimeOutError
-
raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
-
end
-
end
-
-
1
def silenced_unknown_error_message?(msg)
-
silenced_unknown_error_messages.any? { |r| msg =~ r }
-
end
-
-
1
def silenced_unknown_error_messages
-
[ /Error communicating with the remote browser/ ]
-
end
-
end
-
# frozen_string_literal: true
-
1
class Capybara::Selenium::Node < Capybara::Driver::Node
-
1
def visible_text
-
# Selenium doesn't normalize Unicode whitespace.
-
Capybara::Helpers.normalize_whitespace(native.text)
-
end
-
-
1
def all_text
-
text = driver.browser.execute_script("return arguments[0].textContent", native)
-
Capybara::Helpers.normalize_whitespace(text)
-
end
-
-
1
def [](name)
-
native.attribute(name.to_s)
-
rescue Selenium::WebDriver::Error::WebDriverError
-
nil
-
end
-
-
1
def value
-
if tag_name == "select" and multiple?
-
native.find_elements(:xpath, ".//option").select { |n| n.selected? }.map { |n| n[:value] || n.text }
-
else
-
native[:value]
-
end
-
end
-
-
##
-
#
-
# Set the value of the form element to the given value.
-
#
-
# @param [String] value The new value
-
# @param [Hash{}] options Driver specific options for how to set the value
-
# @option options [Symbol,Array] :clear (nil) The method used to clear the previous value <br/>
-
# nil => clear via javascript <br/>
-
# :none => append the new value to the existing value <br/>
-
# :backspace => send backspace keystrokes to clear the field <br/>
-
# Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
-
1
def set(value, options={})
-
tag_name = self.tag_name
-
type = self[:type]
-
if (Array === value) && !multiple?
-
raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
-
end
-
if tag_name == 'input' and type == 'radio'
-
click
-
elsif tag_name == 'input' and type == 'checkbox'
-
click if value ^ native.attribute('checked').to_s.eql?("true")
-
elsif tag_name == 'input' and type == 'file'
-
path_names = value.to_s.empty? ? [] : value
-
if driver.options[:browser].to_s == "chrome"
-
native.send_keys(Array(path_names).join("\n"))
-
else
-
native.send_keys(*path_names)
-
end
-
elsif tag_name == 'textarea' or tag_name == 'input'
-
if readonly?
-
warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
-
elsif value.to_s.empty?
-
native.clear
-
else
-
if options[:clear] == :backspace
-
# Clear field by sending the correct number of backspace keys.
-
backspaces = [:backspace] * self.value.to_s.length
-
native.send_keys(*(backspaces + [value.to_s]))
-
elsif options[:clear] == :none
-
native.send_keys(value.to_s)
-
elsif options[:clear].is_a? Array
-
native.send_keys(*options[:clear], value.to_s)
-
else
-
# Clear field by JavaScript assignment of the value property.
-
# Script can change a readonly element which user input cannot, so
-
# don't execute if readonly.
-
driver.browser.execute_script "arguments[0].value = ''", native
-
native.send_keys(value.to_s)
-
end
-
end
-
elsif native.attribute('isContentEditable')
-
#ensure we are focused on the element
-
script = <<-JS
-
var range = document.createRange();
-
arguments[0].focus();
-
range.selectNodeContents(arguments[0]);
-
window.getSelection().addRange(range);
-
JS
-
driver.browser.execute_script script, native
-
native.send_keys(value.to_s)
-
end
-
end
-
-
1
def select_option
-
native.click unless selected? || disabled?
-
end
-
-
1
def unselect_option
-
if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
-
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
-
end
-
native.click if selected?
-
end
-
-
1
def click
-
native.click
-
end
-
-
1
def right_click
-
driver.browser.action.context_click(native).perform
-
end
-
-
1
def double_click
-
driver.browser.action.double_click(native).perform
-
end
-
-
1
def send_keys(*args)
-
native.send_keys(*args)
-
end
-
-
1
def hover
-
driver.browser.action.move_to(native).perform
-
end
-
-
1
def drag_to(element)
-
driver.browser.action.drag_and_drop(native, element.native).perform
-
end
-
-
1
def tag_name
-
native.tag_name.downcase
-
end
-
-
1
def visible?
-
displayed = native.displayed?
-
displayed and displayed != "false"
-
end
-
-
1
def selected?
-
selected = native.selected?
-
selected and selected != "false"
-
end
-
1
alias :checked? :selected?
-
-
1
def disabled?
-
# workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
-
if driver.marionette?
-
if %w(option optgroup).include? tag_name
-
!native.enabled? || find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
-
else
-
!native.enabled? || !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
-
end
-
else
-
!native.enabled?
-
end
-
end
-
-
1
def readonly?
-
readonly = self[:readonly]
-
readonly and readonly != "false"
-
end
-
-
1
def multiple?
-
multiple = self[:multiple]
-
multiple and multiple != "false"
-
end
-
-
1
def find_xpath(locator)
-
native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
-
end
-
-
1
def find_css(locator)
-
native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
-
end
-
-
1
def ==(other)
-
native == other.native
-
end
-
-
1
def path
-
path = find_xpath('ancestor::*').reverse
-
path.unshift self
-
-
result = []
-
while node = path.shift
-
parent = path.first
-
-
if parent
-
siblings = parent.find_xpath(node.tag_name)
-
if siblings.size == 1
-
result.unshift node.tag_name
-
else
-
index = siblings.index(node)
-
result.unshift "#{node.tag_name}[#{index+1}]"
-
end
-
else
-
result.unshift node.tag_name
-
end
-
end
-
-
'/' + result.join('/')
-
end
-
-
1
private
-
# a reference to the select node if this is an option node
-
1
def select_node
-
find_xpath('./ancestor::select').first
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'uri'
-
1
require 'net/http'
-
1
require 'rack'
-
-
1
module Capybara
-
1
class Server
-
1
class Middleware
-
1
class Counter
-
1
attr_reader :value
-
-
1
def initialize
-
1
@value = 0
-
1
@mutex = Mutex.new
-
end
-
-
1
def increment
-
142
@mutex.synchronize { @value += 1 }
-
end
-
-
1
def decrement
-
142
@mutex.synchronize { @value -= 1 }
-
end
-
end
-
-
1
attr_accessor :error
-
-
1
def initialize(app)
-
1
@app = app
-
1
@counter = Counter.new
-
end
-
-
1
def pending_requests?
-
14
@counter.value > 0
-
end
-
-
1
def call(env)
-
72
if env["PATH_INFO"] == "/__identify__"
-
1
[200, {}, [@app.object_id.to_s]]
-
else
-
71
@counter.increment
-
71
begin
-
71
@app.call(env)
-
rescue *Capybara.server_errors => e
-
@error = e unless @error
-
raise e
-
ensure
-
71
@counter.decrement
-
71
end
-
end
-
end
-
end
-
-
1
class << self
-
1
def ports
-
2
@ports ||= {}
-
end
-
end
-
-
1
attr_reader :app, :port, :host
-
-
1
def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
-
1
@app = app
-
1
@server_thread = nil # suppress warnings
-
1
@host, @port = host, port
-
1
@port ||= Capybara::Server.ports[port_key]
-
1
@port ||= find_available_port(host)
-
end
-
-
1
def reset_error!
-
28
middleware.error = nil
-
end
-
-
1
def error
-
28
middleware.error
-
end
-
-
1
def responsive?
-
3
return false if @server_thread && @server_thread.join(0)
-
-
4
res = Net::HTTP.start(host, port) { |http| http.get('/__identify__') }
-
-
1
if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
-
1
return res.body == app.object_id.to_s
-
end
-
rescue SystemCallError
-
2
return false
-
end
-
-
1
def wait_for_pending_requests
-
28
Timeout.timeout(60) { sleep(0.01) while pending_requests? }
-
rescue Timeout::Error
-
raise "Requests did not finish in 60 seconds"
-
end
-
-
1
def boot
-
1
unless responsive?
-
1
Capybara::Server.ports[port_key] = port
-
-
1
@server_thread = Thread.new do
-
1
Capybara.server.call(middleware, port, host)
-
end
-
-
2
Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
-
end
-
rescue Timeout::Error
-
raise "Rack application timed out during boot"
-
else
-
1
self
-
end
-
-
1
private
-
-
1
def middleware
-
71
@middleware ||= Middleware.new(app)
-
end
-
-
1
def port_key
-
2
Capybara.reuse_server ? app.object_id : middleware.object_id
-
end
-
-
1
def pending_requests?
-
14
middleware.pending_requests?
-
end
-
-
1
def find_available_port(host)
-
1
server = TCPServer.new(host, 0)
-
1
server.addr[1]
-
ensure
-
1
server.close if server
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'capybara/session/matchers'
-
1
require 'addressable/uri'
-
-
1
module Capybara
-
-
##
-
#
-
# The Session class represents a single user's interaction with the system. The Session can use
-
# any of the underlying drivers. A session can be initialized manually like this:
-
#
-
# session = Capybara::Session.new(:culerity, MyRackApp)
-
#
-
# The application given as the second argument is optional. When running Capybara against an external
-
# page, you might want to leave it out:
-
#
-
# session = Capybara::Session.new(:culerity)
-
# session.visit('http://www.google.com')
-
#
-
# Session provides a number of methods for controlling the navigation of the page, such as +visit+,
-
# +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
-
# the current HTML document. This allows interaction:
-
#
-
# session.fill_in('q', with: 'Capybara')
-
# session.click_button('Search')
-
# expect(session).to have_content('Capybara')
-
#
-
# When using capybara/dsl, the Session is initialized automatically for you.
-
#
-
1
class Session
-
1
include Capybara::SessionMatchers
-
-
1
NODE_METHODS = [
-
:all, :first, :attach_file, :text, :check, :choose,
-
:click_link_or_button, :click_button, :click_link, :field_labeled,
-
:fill_in, :find, :find_all, :find_button, :find_by_id, :find_field, :find_link,
-
:has_content?, :has_text?, :has_css?, :has_no_content?, :has_no_text?,
-
:has_no_css?, :has_no_xpath?, :resolve, :has_xpath?, :select, :uncheck,
-
:has_link?, :has_no_link?, :has_button?, :has_no_button?, :has_field?,
-
:has_no_field?, :has_checked_field?, :has_unchecked_field?,
-
:has_no_table?, :has_table?, :unselect, :has_select?, :has_no_select?,
-
:has_selector?, :has_no_selector?, :click_on, :has_no_checked_field?,
-
:has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector,
-
:assert_all_of_selectors, :assert_none_of_selectors,
-
:refute_selector, :assert_text, :assert_no_text
-
]
-
# @api private
-
1
DOCUMENT_METHODS = [
-
:title, :assert_title, :assert_no_title, :has_title?, :has_no_title?
-
]
-
1
SESSION_METHODS = [
-
:body, :html, :source, :current_url, :current_host, :current_path,
-
:execute_script, :evaluate_script, :visit, :go_back, :go_forward,
-
:within, :within_element, :within_fieldset, :within_table, :within_frame, :switch_to_frame,
-
:current_window, :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
-
:save_page, :save_and_open_page, :save_screenshot,
-
:save_and_open_screenshot, :reset_session!, :response_headers,
-
:status_code, :current_scope,
-
:assert_current_path, :assert_no_current_path, :has_current_path?, :has_no_current_path?
-
] + DOCUMENT_METHODS
-
1
MODAL_METHODS = [
-
:accept_alert, :accept_confirm, :dismiss_confirm, :accept_prompt,
-
:dismiss_prompt
-
]
-
1
DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS
-
-
1
attr_reader :mode, :app, :server
-
1
attr_accessor :synchronized
-
-
1
def initialize(mode, app=nil)
-
1
raise TypeError, "The second parameter to Session::new should be a rack app if passed." if app && !app.respond_to?(:call)
-
1
@mode = mode
-
1
@app = app
-
1
if Capybara.run_server and @app and driver.needs_server?
-
1
@server = Capybara::Server.new(@app).boot
-
else
-
@server = nil
-
end
-
1
@touched = false
-
end
-
-
1
def driver
-
@driver ||= begin
-
1
unless Capybara.drivers.has_key?(mode)
-
other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
-
raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
-
end
-
1
Capybara.drivers[mode].call(app)
-
27
end
-
end
-
-
##
-
#
-
# Reset the session (i.e. remove cookies and navigate to blank page)
-
#
-
# This method does not:
-
#
-
# * accept modal dialogs if they are present (Selenium driver now does, others may not)
-
# * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
-
# * modify state of the driver/underlying browser in any other way
-
#
-
# as doing so will result in performance downsides and it's not needed to do everything from the list above for most apps.
-
#
-
# If you want to do anything from the list above on a general basis you can:
-
#
-
# * write RSpec/Cucumber/etc. after hook
-
# * monkeypatch this method
-
# * use Ruby's `prepend` method
-
#
-
1
def reset!
-
14
if @touched
-
10
driver.reset!
-
10
@touched = false
-
end
-
14
@server.wait_for_pending_requests if @server
-
14
raise_server_error!
-
end
-
1
alias_method :cleanup!, :reset!
-
1
alias_method :reset_session!, :reset!
-
-
##
-
#
-
# Raise errors encountered in the server
-
#
-
1
def raise_server_error!
-
28
if Capybara.raise_server_errors and @server and @server.error
-
# Force an explanation for the error being raised as the exception cause
-
begin
-
raise CapybaraError, "Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true"
-
rescue CapybaraError
-
#needed to get the cause set correctly in JRuby -- otherwise we could just do raise @server.error
-
raise @server.error, @server.error.message, @server.error.backtrace
-
end
-
end
-
ensure
-
28
@server.reset_error! if @server
-
end
-
-
##
-
#
-
# Returns a hash of response headers. Not supported by all drivers (e.g. Selenium)
-
#
-
# @return [Hash{String => String}] A hash of response headers.
-
#
-
1
def response_headers
-
driver.response_headers
-
end
-
-
##
-
#
-
# Returns the current HTTP status code as an Integer. Not supported by all drivers (e.g. Selenium)
-
#
-
# @return [Integer] Current HTTP status code
-
#
-
1
def status_code
-
driver.status_code
-
end
-
-
##
-
#
-
# @return [String] A snapshot of the DOM of the current document, as it looks right now (potentially modified by JavaScript).
-
#
-
1
def html
-
driver.html
-
end
-
1
alias_method :body, :html
-
1
alias_method :source, :html
-
-
##
-
#
-
# @return [String] Path of the current page, without any domain information
-
#
-
1
def current_path
-
# Addressable parsing is more lenient than URI
-
1
uri = Addressable::URI.parse(current_url)
-
-
# If current_url ends up being nil, won't be able to call .path on a NilClass.
-
1
return nil if uri.nil?
-
-
# Addressable doesn't support opaque URIs - we want nil here
-
1
return nil if uri.scheme == "about"
-
1
path = uri.path
-
1
path if path and not path.empty?
-
end
-
-
##
-
#
-
# @return [String] Host of the current page
-
#
-
1
def current_host
-
uri = URI.parse(current_url)
-
"#{uri.scheme}://#{uri.host}" if uri.host
-
end
-
-
##
-
#
-
# @return [String] Fully qualified URL of the current page
-
#
-
1
def current_url
-
1
driver.current_url
-
end
-
-
##
-
#
-
# Navigate to the given URL. The URL can either be a relative URL or an absolute URL
-
# The behaviour of either depends on the driver.
-
#
-
# session.visit('/foo')
-
# session.visit('http://google.com')
-
#
-
# For drivers which can run against an external application, such as the selenium driver
-
# giving an absolute URL will navigate to that page. This allows testing applications
-
# running on remote servers. For these drivers, setting {Capybara.app_host} will make the
-
# remote server the default. For example:
-
#
-
# Capybara.app_host = 'http://google.com'
-
# session.visit('/') # visits the google homepage
-
#
-
# If {Capybara.always_include_port} is set to true and this session is running against
-
# a rack application, then the port that the rack application is running on will automatically
-
# be inserted into the URL. Supposing the app is running on port `4567`, doing something like:
-
#
-
# visit("http://google.com/test")
-
#
-
# Will actually navigate to `http://google.com:4567/test`.
-
#
-
# @param [#to_s] visit_uri The URL to navigate to. The parameter will be cast to a String.
-
#
-
1
def visit(visit_uri)
-
14
raise_server_error!
-
14
@touched = true
-
-
14
visit_uri = URI.parse(visit_uri.to_s)
-
-
14
uri_base = if @server
-
14
visit_uri.port = @server.port if Capybara.always_include_port && (visit_uri.port == visit_uri.default_port)
-
14
URI.parse(Capybara.app_host || "http://#{@server.host}:#{@server.port}")
-
else
-
Capybara.app_host && URI.parse(Capybara.app_host)
-
end
-
-
# TODO - this is only for compatability with previous 2.x behavior that concatenated
-
# Capybara.app_host and a "relative" path - Consider removing in 3.0
-
# @abotalov brought up a good point about this behavior potentially being useful to people
-
# deploying to a subdirectory and/or single page apps where only the url fragment changes
-
14
if visit_uri.scheme.nil? && uri_base
-
14
visit_uri.path = uri_base.path + visit_uri.path
-
end
-
-
14
visit_uri = uri_base.merge(visit_uri) unless uri_base.nil?
-
-
14
driver.visit(visit_uri.to_s)
-
end
-
-
##
-
#
-
# Move back a single entry in the browser's history.
-
#
-
1
def go_back
-
driver.go_back
-
end
-
-
##
-
#
-
# Move forward a single entry in the browser's history.
-
#
-
1
def go_forward
-
driver.go_forward
-
end
-
-
##
-
#
-
# Executes the given block within the context of a node. `within` takes the
-
# same options as `find`, as well as a block. For the duration of the
-
# block, any command to Capybara will be handled as though it were scoped
-
# to the given element.
-
#
-
# within(:xpath, './/div[@id="delivery-address"]') do
-
# fill_in('Street', with: '12 Main Street')
-
# end
-
#
-
# Just as with `find`, if multiple elements match the selector given to
-
# `within`, an error will be raised, and just as with `find`, this
-
# behaviour can be controlled through the `:match` and `:exact` options.
-
#
-
# It is possible to omit the first parameter, in that case, the selector is
-
# assumed to be of the type set in Capybara.default_selector.
-
#
-
# within('div#delivery-address') do
-
# fill_in('Street', with: '12 Main Street')
-
# end
-
#
-
# Note that a lot of uses of `within` can be replaced more succinctly with
-
# chaining:
-
#
-
# find('div#delivery-address').fill_in('Street', with: '12 Main Street')
-
#
-
# @overload within(*find_args)
-
# @param (see Capybara::Node::Finders#all)
-
#
-
# @overload within(a_node)
-
# @param [Capybara::Node::Base] a_node The node in whose scope the block should be evaluated
-
#
-
# @raise [Capybara::ElementNotFound] If the scope can't be found before time expires
-
#
-
1
def within(*args)
-
new_scope = if args.first.is_a?(Capybara::Node::Base) then args.first else find(*args) end
-
begin
-
scopes.push(new_scope)
-
yield
-
ensure
-
scopes.pop
-
end
-
end
-
1
alias_method :within_element, :within
-
-
##
-
#
-
# Execute the given block within the a specific fieldset given the id or legend of that fieldset.
-
#
-
# @param [String] locator Id or legend of the fieldset
-
#
-
1
def within_fieldset(locator)
-
within :fieldset, locator do
-
yield
-
end
-
end
-
-
##
-
#
-
# Execute the given block within the a specific table given the id or caption of that table.
-
#
-
# @param [String] locator Id or caption of the table
-
#
-
1
def within_table(locator)
-
within :table, locator do
-
yield
-
end
-
end
-
-
##
-
#
-
# Switch to the given frame
-
#
-
# If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to.
-
# Capybara::Session#within_frame is preferred over this method and should be used when possible.
-
# May not be supported by all drivers.
-
#
-
# @overload switch_to_frame(element)
-
# @param [Capybara::Node::Element] iframe/frame element to switch to
-
# @overload switch_to_frame(:parent)
-
# Switch to the parent element
-
# @overload switch_to_frame(:top)
-
# Switch to the top level document
-
#
-
1
def switch_to_frame(frame)
-
case frame
-
when Capybara::Node::Element
-
driver.switch_to_frame(frame)
-
scopes.push(:frame)
-
when :parent
-
raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's "\
-
"`within` block." if scopes.last() != :frame
-
scopes.pop
-
driver.switch_to_frame(:parent)
-
when :top
-
idx = scopes.index(:frame)
-
if idx
-
raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's "\
-
"`within` block." if scopes.slice(idx..-1).any? {|scope| ![:frame, nil].include?(scope)}
-
scopes.slice!(idx..-1)
-
driver.switch_to_frame(:top)
-
end
-
end
-
end
-
-
##
-
#
-
# Execute the given block within the given iframe using given frame, frame name/id or index.
-
# May not be supported by all drivers.
-
#
-
# @overload within_frame(element)
-
# @param [Capybara::Node::Element] frame element
-
# @overload within_frame([kind = :frame], locator, options = {})
-
# @param [Symobl] kind Optional selector type (:css, :xpath, :field, etc.) - Defaults to :frame
-
# @param [String] locator The locator for the given selector kind. For :frame this is the name/id of a frame/iframe element
-
# @overload within_frame(index)
-
# @param [Integer] index index of a frame (0 based)
-
1
def within_frame(*args)
-
frame = within(document) do # Previous 2.x versions ignored current scope when finding frames - consider changing in 3.0
-
case args[0]
-
when Capybara::Node::Element
-
args[0]
-
when String, Hash
-
find(:frame, *args)
-
when Symbol
-
find(*args)
-
when Integer
-
idx = args[0]
-
all(:frame, minimum: idx+1)[idx]
-
else
-
raise TypeError
-
end
-
end
-
-
begin
-
switch_to_frame(frame)
-
begin
-
yield
-
ensure
-
switch_to_frame(:parent)
-
end
-
rescue Capybara::NotSupportedByDriverError
-
# Support older driver frame API for now
-
if driver.respond_to?(:within_frame)
-
begin
-
scopes.push(:frame)
-
driver.within_frame(frame) do
-
yield
-
end
-
ensure
-
scopes.pop
-
end
-
else
-
raise
-
end
-
end
-
end
-
-
##
-
# @return [Capybara::Window] current window
-
#
-
1
def current_window
-
Window.new(self, driver.current_window_handle)
-
end
-
-
##
-
# Get all opened windows.
-
# The order of windows in returned array is not defined.
-
# The driver may sort windows by their creation time but it's not required.
-
#
-
# @return [Array<Capybara::Window>] an array of all windows
-
#
-
1
def windows
-
driver.window_handles.map do |handle|
-
Window.new(self, handle)
-
end
-
end
-
-
##
-
# Open new window.
-
# Current window doesn't change as the result of this call.
-
# It should be switched to explicitly.
-
#
-
# @return [Capybara::Window] window that has been opened
-
#
-
1
def open_new_window
-
window_opened_by do
-
driver.open_new_window
-
end
-
end
-
-
##
-
# @overload switch_to_window(&block)
-
# Switches to the first window for which given block returns a value other than false or nil.
-
# If window that matches block can't be found, the window will be switched back and `WindowError` will be raised.
-
# @example
-
# window = switch_to_window { title == 'Page title' }
-
# @raise [Capybara::WindowError] if no window matches given block
-
# @overload switch_to_window(window)
-
# @param window [Capybara::Window] window that should be switched to
-
# @raise [Capybara::Driver::Base#no_such_window_error] if unexistent (e.g. closed) window was passed
-
#
-
# @return [Capybara::Window] window that has been switched to
-
# @raise [Capybara::ScopeError] if this method is invoked inside `within`,
-
# `within_frame` or `within_window` methods
-
# @raise [ArgumentError] if both or neither arguments were provided
-
#
-
1
def switch_to_window(window = nil, options= {})
-
options, window = window, nil if window.is_a? Hash
-
-
block_given = block_given?
-
if window && block_given
-
raise ArgumentError, "`switch_to_window` can take either a block or a window, not both"
-
elsif !window && !block_given
-
raise ArgumentError, "`switch_to_window`: either window or block should be provided"
-
elsif scopes.size > 1
-
raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\
-
"`within`'s, `within_frame`'s' or `within_window`'s' block."
-
end
-
-
if window
-
driver.switch_to_window(window.handle)
-
window
-
else
-
wait_time = Capybara::Queries::BaseQuery.wait(options)
-
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
-
original_window_handle = driver.current_window_handle
-
begin
-
driver.window_handles.each do |handle|
-
driver.switch_to_window handle
-
if yield
-
return Window.new(self, handle)
-
end
-
end
-
rescue => e
-
driver.switch_to_window(original_window_handle)
-
raise e
-
else
-
driver.switch_to_window(original_window_handle)
-
raise Capybara::WindowError, "Could not find a window matching block/lambda"
-
end
-
end
-
end
-
end
-
-
##
-
# This method does the following:
-
#
-
# 1. Switches to the given window (it can be located by window instance/lambda/string).
-
# 2. Executes the given block (within window located at previous step).
-
# 3. Switches back (this step will be invoked even if exception will happen at second step)
-
#
-
# @overload within_window(window) { do_something }
-
# @param window [Capybara::Window] instance of `Capybara::Window` class
-
# that will be switched to
-
# @raise [driver#no_such_window_error] if unexistent (e.g. closed) window was passed
-
# @overload within_window(proc_or_lambda) { do_something }
-
# @param lambda [Proc] lambda. First window for which lambda
-
# returns a value other than false or nil will be switched to.
-
# @example
-
# within_window(->{ page.title == 'Page title' }) { click_button 'Submit' }
-
# @raise [Capybara::WindowError] if no window matching lambda was found
-
# @overload within_window(string) { do_something }
-
# @deprecated Pass window or lambda instead
-
# @param [String] handle, name, url or title of the window
-
#
-
# @raise [Capybara::ScopeError] if this method is invoked inside `within`,
-
# `within_frame` or `within_window` methods
-
# @return value returned by the block
-
#
-
1
def within_window(window_or_handle)
-
if window_or_handle.instance_of?(Capybara::Window)
-
original = current_window
-
switch_to_window(window_or_handle) unless original == window_or_handle
-
scopes << nil
-
begin
-
yield
-
ensure
-
@scopes.pop
-
switch_to_window(original) unless original == window_or_handle
-
end
-
elsif window_or_handle.is_a?(Proc)
-
original = current_window
-
switch_to_window { window_or_handle.call }
-
scopes << nil
-
begin
-
yield
-
ensure
-
@scopes.pop
-
switch_to_window(original)
-
end
-
else
-
offending_line = caller.first
-
file_line = offending_line.match(/^(.+?):(\d+)/)[0]
-
warn "DEPRECATION WARNING: Passing string argument to #within_window is deprecated. "\
-
"Pass window object or lambda. (called from #{file_line})"
-
begin
-
scopes << nil
-
driver.within_window(window_or_handle) { yield }
-
ensure
-
@scopes.pop
-
end
-
end
-
end
-
-
##
-
# Get the window that has been opened by the passed block.
-
# It will wait for it to be opened (in the same way as other Capybara methods wait).
-
# It's better to use this method than `windows.last`
-
# {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}
-
#
-
# @param options [Hash]
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) maximum wait time
-
# @return [Capybara::Window] the window that has been opened within a block
-
# @raise [Capybara::WindowError] if block passed to window hasn't opened window
-
# or opened more than one window
-
#
-
1
def window_opened_by(options = {}, &block)
-
old_handles = driver.window_handles
-
block.call
-
-
wait_time = Capybara::Queries::BaseQuery.wait(options)
-
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
-
opened_handles = (driver.window_handles - old_handles)
-
if opened_handles.size != 1
-
raise Capybara::WindowError, "block passed to #window_opened_by "\
-
"opened #{opened_handles.size} windows instead of 1"
-
end
-
Window.new(self, opened_handles.first)
-
end
-
end
-
-
##
-
#
-
# Execute the given script, not returning a result. This is useful for scripts that return
-
# complex objects, such as jQuery statements. +execute_script+ should be used over
-
# +evaluate_script+ whenever possible.
-
#
-
# @param [String] script A string of JavaScript to execute
-
# @param args Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
-
#
-
1
def execute_script(script, *args)
-
@touched = true
-
if driver.method(:execute_script).arity == 1
-
raise Capybara::NotSupportedByDriverError, "The current driver does not support arguments being passed with execute_script" unless args.empty?
-
driver.execute_script(script)
-
else
-
driver.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg} )
-
end
-
end
-
-
##
-
#
-
# Evaluate the given JavaScript and return the result. Be careful when using this with
-
# scripts that return complex objects, such as jQuery statements. +execute_script+ might
-
# be a better alternative.
-
#
-
# @param [String] script A string of JavaScript to evaluate
-
# @return [Object] The result of the evaluated JavaScript (may be driver specific)
-
#
-
1
def evaluate_script(script, *args)
-
@touched = true
-
if driver.method(:evaluate_script).arity == 1
-
raise Capybara::NotSupportedByDriverError, "The current driver does not support arguments being passed with execute_script" unless args.empty?
-
driver.evaluate_script(script)
-
else
-
driver.evaluate_script(script, *args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg} )
-
end
-
end
-
-
##
-
#
-
# Execute the block, accepting a alert.
-
#
-
# @!macro modal_params
-
# @overload $0(text, options = {}, &blk)
-
# @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched
-
# @overload $0(options = {}, &blk)
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time to wait for the modal to appear after executing the block.
-
# @return [String] the message shown in the modal
-
# @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
-
#
-
1
def accept_alert(text_or_options=nil, options={}, &blk)
-
accept_modal(:alert, text_or_options, options, &blk)
-
end
-
-
##
-
#
-
# Execute the block, accepting a confirm.
-
#
-
# @macro modal_params
-
#
-
1
def accept_confirm(text_or_options=nil, options={}, &blk)
-
accept_modal(:confirm, text_or_options, options, &blk)
-
end
-
-
##
-
#
-
# Execute the block, dismissing a confirm.
-
#
-
# @macro modal_params
-
#
-
1
def dismiss_confirm(text_or_options=nil, options={}, &blk)
-
dismiss_modal(:confirm, text_or_options, options, &blk)
-
end
-
-
##
-
#
-
# Execute the block, accepting a prompt, optionally responding to the prompt.
-
#
-
# @macro modal_params
-
# @option options [String] :with Response to provide to the prompt
-
#
-
1
def accept_prompt(text_or_options=nil, options={}, &blk)
-
accept_modal(:prompt, text_or_options, options, &blk)
-
end
-
-
##
-
#
-
# Execute the block, dismissing a prompt.
-
#
-
# @macro modal_params
-
#
-
1
def dismiss_prompt(text_or_options=nil, options={}, &blk)
-
dismiss_modal(:prompt, text_or_options, options, &blk)
-
end
-
-
##
-
#
-
# Save a snapshot of the page. If `Capybara.asset_host` is set it will inject `base` tag
-
# pointing to `asset_host`.
-
#
-
# If invoked without arguments it will save file to `Capybara.save_path`
-
# and file will be given randomly generated filename. If invoked with a relative path
-
# the path will be relative to `Capybara.save_path`, which is different from
-
# the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
-
# relative to Dir.pwd
-
#
-
# @param [String] path the path to where it should be saved
-
# @return [String] the path to which the file was saved
-
#
-
1
def save_page(path = nil)
-
path = prepare_path(path, 'html')
-
File.write(path, Capybara::Helpers.inject_asset_host(body), mode: 'wb')
-
path
-
end
-
-
##
-
#
-
# Save a snapshot of the page and open it in a browser for inspection.
-
#
-
# If invoked without arguments it will save file to `Capybara.save_path`
-
# and file will be given randomly generated filename. If invoked with a relative path
-
# the path will be relative to `Capybara.save_path`, which is different from
-
# the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
-
# relative to Dir.pwd
-
#
-
# @param [String] path the path to where it should be saved
-
#
-
1
def save_and_open_page(path = nil)
-
path = save_page(path)
-
open_file(path)
-
end
-
-
##
-
#
-
# Save a screenshot of page.
-
#
-
# If invoked without arguments it will save file to `Capybara.save_path`
-
# and file will be given randomly generated filename. If invoked with a relative path
-
# the path will be relative to `Capybara.save_path`, which is different from
-
# the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
-
# relative to Dir.pwd
-
#
-
# @param [String] path the path to where it should be saved
-
# @param [Hash] options a customizable set of options
-
# @return [String] the path to which the file was saved
-
1
def save_screenshot(path = nil, options = {})
-
path = prepare_path(path, 'png')
-
driver.save_screenshot(path, options)
-
path
-
end
-
-
##
-
#
-
# Save a screenshot of the page and open it for inspection.
-
#
-
# If invoked without arguments it will save file to `Capybara.save_path`
-
# and file will be given randomly generated filename. If invoked with a relative path
-
# the path will be relative to `Capybara.save_path`, which is different from
-
# the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
-
# relative to Dir.pwd
-
#
-
# @param [String] path the path to where it should be saved
-
# @param [Hash] options a customizable set of options
-
#
-
1
def save_and_open_screenshot(path = nil, options = {})
-
path = save_screenshot(path, options)
-
open_file(path)
-
end
-
-
1
def document
-
79
@document ||= Capybara::Node::Document.new(self, driver)
-
end
-
-
1
NODE_METHODS.each do |method|
-
54
define_method method do |*args, &block|
-
79
@touched = true
-
79
current_scope.send(method, *args, &block)
-
end
-
end
-
-
1
DOCUMENT_METHODS.each do |method|
-
5
define_method method do |*args, &block|
-
document.send(method, *args, &block)
-
end
-
end
-
-
1
def inspect
-
%(#<Capybara::Session>)
-
end
-
-
1
def current_scope
-
79
scope = scopes.last
-
79
scope = document if [nil, :frame].include? scope
-
79
scope
-
end
-
-
1
private
-
1
def accept_modal(type, text_or_options, options, &blk)
-
driver.accept_modal(type, modal_options(text_or_options, options), &blk)
-
end
-
-
1
def dismiss_modal(type, text_or_options, options, &blk)
-
driver.dismiss_modal(type, modal_options(text_or_options, options), &blk)
-
end
-
-
1
def modal_options(text_or_options, options)
-
text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
-
options[:text] ||= text_or_options unless text_or_options.nil?
-
options[:wait] ||= Capybara.default_max_wait_time
-
options
-
end
-
-
-
1
def open_file(path)
-
begin
-
require "launchy"
-
Launchy.open(path)
-
rescue LoadError
-
warn "File saved to #{path}."
-
warn "Please install the launchy gem to open the file automatically."
-
end
-
end
-
-
1
def prepare_path(path, extension)
-
if Capybara.save_path || Capybara.save_and_open_page_path.nil?
-
path = File.expand_path(path || default_fn(extension), Capybara.save_path)
-
else
-
path = File.expand_path(default_fn(extension), Capybara.save_and_open_page_path) if path.nil?
-
end
-
FileUtils.mkdir_p(File.dirname(path))
-
path
-
end
-
-
1
def default_fn(extension)
-
timestamp = Time.new.strftime("%Y%m%d%H%M%S")
-
"capybara-#{timestamp}#{rand(10**10)}.#{extension}"
-
end
-
-
1
def scopes
-
79
@scopes ||= [nil]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
module SessionMatchers
-
##
-
# Asserts that the page has the given path.
-
# By default this will compare against the path+query portion of the full url
-
#
-
# @!macro current_path_query_params
-
# @overload $0(string, options = {})
-
# @param string [String] The string that the current 'path' should equal
-
# @overload $0(regexp, options = {})
-
# @param regexp [Regexp] The regexp that the current 'path' should match to
-
# @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current path to eq/match given string/regexp argument
-
# @option options [Boolean] :url (false) Whether the compare should be done against the full url
-
# @option options [Boolean] :only_path (false) Whether the compare should be done against just the path protion of the url
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_current_path(path, options={})
-
_verify_current_path(path,options) { |query| raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self) }
-
end
-
-
##
-
# Asserts that the page doesn't have the given path.
-
#
-
# @macro current_path_query_params
-
# @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
-
# @return [true]
-
#
-
1
def assert_no_current_path(path, options={})
-
_verify_current_path(path,options) { |query| raise Capybara::ExpectationNotMet, query.negative_failure_message if query.resolves_for?(self) }
-
end
-
-
##
-
# Checks if the page has the given path.
-
#
-
# @macro current_path_query_params
-
# @return [Boolean]
-
#
-
1
def has_current_path?(path, options={})
-
assert_current_path(path, options)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
##
-
# Checks if the page doesn't have the given path.
-
#
-
# @macro current_path_query_params
-
# @return [Boolean]
-
#
-
1
def has_no_current_path?(path, options={})
-
assert_no_current_path(path, options)
-
rescue Capybara::ExpectationNotMet
-
return false
-
end
-
-
1
private
-
-
1
def _verify_current_path(path, options)
-
query = Capybara::Queries::CurrentPathQuery.new(path, options)
-
document.synchronize(query.wait) do
-
yield(query)
-
end
-
return true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
1
VERSION = '2.12.0'
-
end
-
# frozen_string_literal: true
-
1
module Capybara
-
##
-
# The Window class represents a browser window.
-
#
-
# You can get an instance of the class by calling either of:
-
#
-
# * {Capybara::Session#windows}
-
# * {Capybara::Session#current_window}
-
# * {Capybara::Session#window_opened_by}
-
# * {Capybara::Session#switch_to_window}
-
#
-
# Note that some drivers (e.g. Selenium) support getting size of/resizing/closing only
-
# current window. So if you invoke such method for:
-
#
-
# * window that is current, Capybara will make 2 Selenium method invocations
-
# (get handle of current window + get size/resize/close).
-
# * window that is not current, Capybara will make 4 Selenium method invocations
-
# (get handle of current window + switch to given handle + get size/resize/close + switch to original handle)
-
#
-
1
class Window
-
# @return [String] a string that uniquely identifies window within session
-
1
attr_reader :handle
-
-
# @return [Capybara::Session] session that this window belongs to
-
1
attr_reader :session
-
-
# @api private
-
1
def initialize(session, handle)
-
@session = session
-
@driver = session.driver
-
@handle = handle
-
end
-
-
##
-
# @return [Boolean] whether the window is not closed
-
1
def exists?
-
@driver.window_handles.include?(@handle)
-
end
-
-
##
-
# @return [Boolean] whether the window is closed
-
1
def closed?
-
!exists?
-
end
-
-
##
-
# @return [Boolean] whether this window is the window in which commands are being executed
-
1
def current?
-
@driver.current_window_handle == @handle
-
rescue @driver.no_such_window_error
-
false
-
end
-
-
##
-
# Close window.
-
#
-
# If this method was called for window that is current, then after calling this method
-
# future invocations of other Capybara methods should raise
-
# `session.driver.no_such_window_error` until another window will be switched to.
-
#
-
# @!macro about_current
-
# If this method was called for window that is not current, then after calling this method
-
# current window shouldn remain the same as it was before calling this method.
-
#
-
1
def close
-
@driver.close_window(handle)
-
end
-
-
##
-
# Get window size.
-
#
-
# @macro about_current
-
# @return [Array<(Integer, Integer)>] an array with width and height
-
#
-
1
def size
-
@driver.window_size(handle)
-
end
-
-
##
-
# Resize window.
-
#
-
# @macro about_current
-
# @param width [String] the new window width in pixels
-
# @param height [String] the new window height in pixels
-
#
-
1
def resize_to(width, height)
-
wait_for_stable_size { @driver.resize_window_to(handle, width, height) }
-
end
-
-
##
-
# Maximize window.
-
#
-
# If a particular driver (e.g. headless driver) doesn't have concept of maximizing it
-
# may not support this method.
-
#
-
# @macro about_current
-
#
-
1
def maximize
-
wait_for_stable_size { @driver.maximize_window(handle) }
-
end
-
-
1
def eql?(other)
-
other.kind_of?(self.class) && @session == other.session && @handle == other.handle
-
end
-
1
alias_method :==, :eql?
-
-
1
def hash
-
@session.hash ^ @handle.hash
-
end
-
-
1
def inspect
-
"#<Window @handle=#{@handle.inspect}>"
-
end
-
-
1
private
-
-
1
def wait_for_stable_size(seconds=Capybara.default_max_wait_time)
-
res = yield if block_given?
-
prev_size = size
-
start_time = Capybara::Helpers.monotonic_time
-
begin
-
sleep 0.05
-
cur_size = size
-
return res if cur_size == prev_size
-
prev_size = cur_size
-
end while (Capybara::Helpers.monotonic_time - start_time) < seconds
-
#TODO raise error in 3.0
-
#raise Capybara::WindowError, "Window size not stable."
-
warn "Window size not stable in #{seconds} seconds. This will raise an exception in a future version of Capybara"
-
return res
-
end
-
-
1
def raise_unless_current(what)
-
unless current?
-
raise Capybara::WindowError, "#{what} not current window is not possible."
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
-
1
require File.expand_path('../core_ext/file', __FILE__)
-
-
1
require 'cliver/version'
-
1
require 'cliver/dependency'
-
1
require 'cliver/shell_capture'
-
1
require 'cliver/detector'
-
1
require 'cliver/filter'
-
-
# Cliver is tool for making dependency assertions against
-
# command-line executables.
-
1
module Cliver
-
-
# The primary interface for the Cliver gem allows detection of an executable
-
# on your path that matches a version requirement, or raise an appropriate
-
# exception to make resolution simple and straight-forward.
-
# @see Cliver::Dependency
-
# @overload (see Cliver::Dependency#initialize)
-
# @param (see Cliver::Dependency#initialize)
-
# @raise (see Cliver::Dependency#detect!)
-
# @return (see Cliver::Dependency#detect!)
-
1
def self.detect!(*args, &block)
-
Dependency::new(*args, &block).detect!
-
end
-
-
# A non-raising variant of {::detect!}, simply returns false if dependency
-
# cannot be found.
-
# @see Cliver::Dependency
-
# @overload (see Cliver::Dependency#initialize)
-
# @param (see Cliver::Dependency#initialize)
-
# @raise (see Cliver::Dependency#detect)
-
# @return (see Cliver::Dependency#detect)
-
1
def self.detect(*args, &block)
-
1
Dependency::new(*args, &block).detect
-
end
-
-
# A legacy interface for {::detect} with the option `strict: true`, ensures
-
# that the first executable on your path matches the requirements.
-
# @see Cliver::Dependency
-
# @overload (see Cliver::Dependency#initialize)
-
# @param (see Cliver::Dependency#initialize)
-
# @option options [Boolean] :strict (true) @see Cliver::Dependency::initialize
-
# @raise (see Cliver::Dependency#detect!)
-
# @return (see Cliver::Dependency#detect!)
-
1
def self.assert(*args, &block)
-
options = args.last.kind_of?(Hash) ? args.pop : {}
-
args << options.merge(:strict => true)
-
Dependency::new(*args, &block).detect!
-
end
-
-
# Verify an absolute-path to an executable.
-
# @overload verify!(executable, *requirements, options = {})
-
# @param executable [String] absolute path to an executable
-
# @param requirements (see Cliver::Dependency#initialize)
-
# @option options (see Cliver::Dependency::initialize)
-
# @raise (see Cliver::Dependency#detect!)
-
# @return (see Cliver::Dependency#detect!)
-
1
def self.verify!(executable, *args, &block)
-
unless File.absolute_path?(executable)
-
raise ArgumentError, "executable path must be absolute, " +
-
"got '#{executable.inspect}'."
-
end
-
options = args.last.kind_of?(Hash) ? args.pop : {}
-
args << options.merge(:path => '.') # ensure path non-empty.
-
Dependency::new(executable, *args, &block).detect!
-
end
-
-
1
extend self
-
-
# Wraps Cliver::assert and returns truthy/false instead of raising
-
# @see Cliver::assert
-
# @overload (see Cliver::Assertion#initialize)
-
# @param (see Cliver::Assertion#initialize)
-
# @return [False,String] either returns false or the reason why the
-
# assertion was unmet.
-
1
def dependency_unmet?(*args, &block)
-
Cliver.assert(*args, &block)
-
false
-
rescue Dependency::NotMet => error
-
# Cliver::Assertion::VersionMismatch -> 'Version Mismatch'
-
reason = error.class.name.split(':').last.gsub(/([a-z])([A-Z])/, '\\1 \\2')
-
"#{reason}: #{error.message}"
-
end
-
end
-
# encoding: utf-8
-
1
require 'rubygems/requirement'
-
1
require 'set'
-
-
1
module Cliver
-
# This is how a dependency is specified.
-
1
class Dependency
-
-
# An exception class raised when assertion is not met
-
1
NotMet = Class.new(ArgumentError)
-
-
# An exception that is raised when executable present, but
-
# no version that matches the requirements is present.
-
1
VersionMismatch = Class.new(Dependency::NotMet)
-
-
# An exception that is raised when executable is not present at all.
-
1
NotFound = Class.new(Dependency::NotMet)
-
-
# A pattern for extracting a {Gem::Version}-parsable version
-
1
PARSABLE_GEM_VERSION = /[0-9]+(.[0-9]+){0,4}(.[a-zA-Z0-9]+)?/.freeze
-
-
# @overload initialize(executables, *requirements, options = {})
-
# @param executables [String,Array<String>] api-compatible executable names
-
# e.g, ['python2','python']
-
# @param requirements [Array<String>, String] splat of strings
-
# whose elements follow the pattern
-
# [<operator>] <version>
-
# Where <operator> is optional (default '='') and in the set
-
# '=', '!=', '>', '<', '>=', '<=', or '~>'
-
# And <version> is dot-separated integers with optional
-
# alphanumeric pre-release suffix. See also
-
# {http://docs.rubygems.org/read/chapter/16 Specifying Versions}
-
# @param options [Hash<Symbol,Object>]
-
# @option options [Cliver::Detector] :detector (Detector.new)
-
# @option options [#to_proc, Object] :detector (see Detector::generate)
-
# @option options [#to_proc] :filter ({Cliver::Filter::IDENTITY})
-
# @option options [Boolean] :strict (false)
-
# true - fail if first match on path fails
-
# to meet version requirements.
-
# This is used for Cliver::assert.
-
# false - continue looking on path until a
-
# sufficient version is found.
-
# @option options [String] :path ('*') the path on which to search
-
# for executables. If an asterisk (`*`) is
-
# included in the supplied string, it is
-
# replaced with `ENV['PATH']`
-
#
-
# @yieldparam executable_path [String] (see Detector#detect_version)
-
# @yieldreturn [String] containing a version that, once filtered, can be
-
# used for comparrison.
-
1
def initialize(executables, *args, &detector)
-
1
options = args.last.kind_of?(Hash) ? args.pop : {}
-
1
@detector = Detector::generate(detector || options[:detector])
-
1
@filter = options.fetch(:filter, Filter::IDENTITY).extend(Filter)
-
1
@path = options.fetch(:path, '*')
-
1
@strict = options.fetch(:strict, false)
-
-
1
@executables = Array(executables).dup.freeze
-
1
@requirement = args unless args.empty?
-
-
1
check_compatibility!
-
end
-
-
# One of these things is not like the other ones...
-
# Some feature combinations just aren't compatible. This method ensures
-
# the the features selected for this object are compatible with each-other.
-
# @return [void]
-
# @raise [ArgumentError] if incompatibility found
-
1
def check_compatibility!
-
case
-
1
when @executables.any? {|exe| exe[File::SEPARATOR] && !File.absolute_path?(exe) }
-
# if the executable contains a path component, it *must* be absolute.
-
raise ArgumentError, "Relative-path executable requirements are not supported."
-
1
end
-
end
-
-
# Get all the installed versions of the api-compatible executables.
-
# If a block is given, it yields once per found executable, lazily.
-
# @yieldparam executable_path [String]
-
# @yieldparam version [String]
-
# @yieldreturn [Boolean] - true if search should stop.
-
# @return [Hash<String,String>] executable_path, version
-
1
def installed_versions
-
2
return enum_for(:installed_versions) unless block_given?
-
-
1
find_executables.each do |executable_path|
-
1
version = detect_version(executable_path)
-
-
1
break(2) if yield(executable_path, version)
-
end
-
end
-
-
# The non-raise variant of {#detect!}
-
# @return (see #detect!)
-
# or nil if no match found.
-
1
def detect
-
1
detect!
-
rescue Dependency::NotMet
-
nil
-
end
-
-
# Detects an installed version of the executable that matches the
-
# requirements.
-
# @return [String] path to an executable that meets the requirements
-
# @raise [Cliver::Dependency::NotMet] if no match found
-
1
def detect!
-
1
installed = {}
-
1
installed_versions.each do |path, version|
-
1
installed[path] = version
-
1
return path if ENV['CLIVER_NO_VERIFY']
-
1
return path if requirement_satisfied_by?(version)
-
strict?
-
end
-
-
# dependency not met. raise the appropriate error.
-
raise_not_found! if installed.empty?
-
raise_version_mismatch!(installed)
-
end
-
-
1
private
-
-
# @api private
-
# @return [Gem::Requirement]
-
1
def filtered_requirement
-
@filtered_requirement ||= begin
-
1
Gem::Requirement.new(@filter.requirements(@requirement))
-
1
end
-
end
-
-
# @api private
-
# @param raw_version [String]
-
# @return [Boolean]
-
1
def requirement_satisfied_by?(raw_version)
-
1
return true unless @requirement
-
1
parsable_version = @filter.apply(raw_version)[PARSABLE_GEM_VERSION]
-
1
parsable_version || raise(ArgumentError) # TODO: make descriptive
-
1
filtered_requirement.satisfied_by? Gem::Version.new(parsable_version)
-
end
-
-
# @api private
-
# @raise [Cliver::Dependency::NotFound] with appropriate error message
-
1
def raise_not_found!
-
raise Dependency::NotFound.new(
-
"Could not find an executable #{@executables} on your path.")
-
end
-
-
# @api private
-
# @raise [Cliver::Dependency::VersionMismatch] with appropriate error message
-
# @param installed [Hash<String,String>] the found versions
-
1
def raise_version_mismatch!(installed)
-
raise Dependency::VersionMismatch.new(
-
"Could not find an executable #{executable_description} that " +
-
"matched the requirements #{requirements_description}. " +
-
"Found versions were #{installed.inspect}.")
-
end
-
-
# @api private
-
# @return [String] a plain-language representation of the executables
-
# for which we were searching
-
1
def executable_description
-
quoted_exes = @executables.map {|exe| "'#{exe}'" }
-
return quoted_exes.first if quoted_exes.size == 1
-
-
last_quoted_exec = quoted_exes.pop
-
"#{quoted_exes.join(', ')} or #{last_quoted_exec}"
-
end
-
-
# @api private
-
# @return [String] a plain-language representation of the requirements
-
1
def requirements_description
-
@requirement.map {|req| "'#{req}'" }.join(', ')
-
end
-
-
# If strict? is true, only attempt the first matching executable on the path
-
# @api private
-
# @return [Boolean]
-
1
def strict?
-
1
false | @strict
-
end
-
-
# Given a path to an executable, detect its version
-
# @api private
-
# @param executable_path [String]
-
# @return [String]
-
# @raise [ArgumentError] if version cannot be detected.
-
1
def detect_version(executable_path)
-
# No need to shell out if we are only checking its presence.
-
1
return '99.version_detection_not_required' unless @requirement
-
-
1
raw_version = @detector.to_proc.call(executable_path)
-
raw_version || raise(ArgumentError,
-
"The detector #{@detector} failed to detect the" +
-
1
"version of the executable at '#{executable_path}'")
-
end
-
-
# Analog of Windows `where` command, or a `which` that finds *all*
-
# matching executables on the supplied path.
-
# @return [Enumerable<String>] - the executables found, lazily.
-
1
def find_executables
-
2
return enum_for(:find_executables) unless block_given?
-
-
1
exts = (ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : []) << ''
-
1
paths = @path.sub('*', ENV['PATH']).split(File::PATH_SEPARATOR)
-
1
raise ArgumentError.new('No PATH to search!') if paths.empty?
-
1
cmds = strict? ? @executables.first(1) : @executables
-
-
1
lookup_cache = Set.new
-
1
cmds.product(paths, exts).map do |cmd, path, ext|
-
6
exe = File.absolute_path?(cmd) ? cmd : File.expand_path("#{cmd}#{ext}", path)
-
-
6
next unless lookup_cache.add?(exe) # don't yield the same exe path 2x
-
6
next unless File.executable?(exe)
-
-
1
yield exe
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
1
require 'shellwords'
-
-
1
module Cliver
-
# Default implementation of the detector needed by Cliver::Assertion,
-
# which will take anything that #respond_to?(:to_proc)
-
1
class Detector < Struct.new(:command_arg, :version_pattern)
-
# @param detector_argument [#call, Object]
-
# If detector_argument responds to #call, return it; otherwise attempt
-
# to create an instance of self.
-
1
def self.generate(detector_argument)
-
1
return detector_argument if detector_argument.respond_to?(:call)
-
1
new(*Array(detector_argument))
-
end
-
-
# Default pattern to use when searching {#version_command} output
-
1
DEFAULT_VERSION_PATTERN = /(version ?)?[0-9][.0-9a-z]+/i.freeze
-
-
# Default command argument to use against the executable to get
-
# version output
-
1
DEFAULT_COMMAND_ARG = '--version'.freeze
-
-
# Forgiving input, allows either argument if only one supplied.
-
#
-
# @overload initialize(*command_args)
-
# @param command_args [Array<String>]
-
# @overload initialize(version_pattern)
-
# @param version_pattern [Regexp]
-
# @overload initialize(*command_args, version_pattern)
-
# @param command_args [Array<String>]
-
# @param version_pattern [Regexp]
-
1
def initialize(*args)
-
1
version_pattern = args.pop if args.last.kind_of?(Regexp)
-
1
command_args = args unless args.empty?
-
-
1
super(command_args, version_pattern)
-
end
-
-
# @param executable_path [String] - the path to the executable to test
-
# @return [String] - should be contain {Gem::Version}-parsable
-
# version number.
-
1
def detect_version(executable_path)
-
1
capture = ShellCapture.new(version_command(executable_path))
-
1
unless capture.command_found
-
raise Cliver::Dependency::NotFound.new(
-
"Could not find an executable at given path '#{executable_path}'." +
-
"If this path was not specified explicitly, it is probably a " +
-
"bug in [Cliver](https://github.com/yaauie/cliver/issues)."
-
)
-
end
-
1
capture.stdout[version_pattern] || capture.stderr[version_pattern]
-
end
-
-
# This is the interface that any detector must have.
-
# If not overridden, returns a proc that wraps #detect_version
-
# @see #detect_version
-
# @return [Proc] following method signature of {#detect_version}
-
1
def to_proc
-
1
method(:detect_version).to_proc
-
end
-
-
# The pattern to match the version in {#version_command}'s output.
-
# Defaults to {DEFAULT_VERSION_PATTERN}
-
# @return [Regexp] - the pattern used against the output
-
# of the #version_command, which should
-
# contain a {Gem::Version}-parsable substring.
-
1
def version_pattern
-
1
super || DEFAULT_VERSION_PATTERN
-
end
-
-
# The argument to pass to the executable to get current version
-
# Defaults to {DEFAULT_COMMAND_ARG}
-
# @return [String, Array<String>]
-
1
def command_arg
-
1
super || DEFAULT_COMMAND_ARG
-
end
-
-
# @param executable_path [String] the executable to test
-
# @return [Array<String>]
-
1
def version_command(executable_path)
-
1
[executable_path, *Array(command_arg)]
-
end
-
end
-
end
-
# encoding: utf-8
-
-
1
module Cliver
-
# A Namespace to hold filter procs
-
1
module Filter
-
# The identity filter returns its input unchanged.
-
4
IDENTITY = proc { |version| version }
-
-
# Apply to a list of requirements
-
# @param requirements [Array<String>]
-
# @return [Array<String>]
-
1
def requirements(requirements)
-
1
requirements.map do |requirement|
-
2
req_parts = requirement.split(/\b(?=\d)/, 2)
-
2
version = req_parts.last
-
2
version.replace apply(version)
-
2
req_parts.join
-
end
-
end
-
-
# Apply to some input
-
# @param version [String]
-
# @return [String]
-
1
def apply(version)
-
3
to_proc.call(version)
-
end
-
end
-
end
-
1
require 'open3'
-
-
1
module Cliver
-
1
class ShellCapture
-
1
attr_reader :stdout, :stderr, :command_found
-
-
# @overlaod initialize(command)
-
# @param command [String] the command to run
-
# @overload initialize(command)
-
# @param command [Array<String>] the command to run; elements in
-
# the supplied array will be shelljoined.
-
# @return [void]
-
1
def initialize(command)
-
1
command = command.shelljoin if command.kind_of?(Array)
-
1
@stdout = @stderr = ''
-
1
begin
-
1
Open3.popen3(command) do |i, o, e|
-
1
@stdout = o.read.chomp
-
1
@stderr = e.read.chomp
-
end
-
# Fix for ruby 1.8.7 (and probably earlier):
-
# Open3.popen3 does not raise anything there, but the error goes to STDERR.
-
if @stderr =~ /open3.rb:\d+:in `exec': No such file or directory -.*\(Errno::ENOENT\)/ or
-
1
@stderr =~ /An exception occurred in a forked block\W+No such file or directory.*\(Errno::ENOENT\)/
-
@stderr = ''
-
@command_found = false
-
else
-
1
@command_found = true
-
end
-
rescue Errno::ENOENT, IOError
-
@command_found = false
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
-
1
module Cliver
-
# Cliver follows {http://semver.org SemVer}
-
1
VERSION = '0.3.2'
-
end
-
# encoding: utf-8
-
-
# Core-Extensions on File
-
1
class File
-
# determine whether a String path is absolute.
-
# @example
-
# File.absolute_path?('foo') #=> false
-
# File.absolute_path?('/foo') #=> true
-
# File.absolute_path?('foo/bar') #=> false
-
# File.absolute_path?('/foo/bar') #=> true
-
# File.absolute_path?('C:foo/bar') #=> false
-
# File.absolute_path?('C:/foo/bar') #=> true
-
# @param path [String] - a pathname
-
# @return [Boolean]
-
1
def self.absolute_path?(path, platform = :default)
-
6
pattern = case platform
-
6
when :default then ABSOLUTE_PATH_PATTERN
-
when :windows then WINDOWS_ABSOLUTE_PATH_PATTERN
-
when :posix then POSIX_ABSOLUTE_PATH_PATTERN
-
else raise ArgumentError, "Unsupported platform '#{platform.inspect}'"
-
end
-
-
6
false | path[pattern]
-
end
-
-
1
unless defined?(POSIX_ABSOLUTE_PATH_PATTERN)
-
1
POSIX_ABSOLUTE_PATH_PATTERN = /\A\//.freeze
-
end
-
-
1
unless defined?(WINDOWS_ABSOLUTE_PATH_PATTERN)
-
1
WINDOWS_ABSOLUTE_PATH_PATTERN = Regexp.union(
-
POSIX_ABSOLUTE_PATH_PATTERN,
-
/\A([A-Z]:)?(\\|\/)/i
-
).freeze
-
end
-
-
ABSOLUTE_PATH_PATTERN = begin
-
1
File::ALT_SEPARATOR ?
-
WINDOWS_ABSOLUTE_PATH_PATTERN :
-
POSIX_ABSOLUTE_PATH_PATTERN
-
1
end unless defined?(ABSOLUTE_PATH_PATTERN)
-
end
-
%w[
-
dm-core
-
dm-aggregates
-
dm-constraints
-
dm-migrations
-
dm-transactions
-
dm-serializer
-
dm-timestamps
-
dm-validations
-
dm-types
-
1
].each do |lib|
-
9
require lib
-
end
-
1
require 'data_objects/version'
-
1
require 'data_objects/utilities'
-
1
require 'data_objects/logger'
-
1
require 'data_objects/byte_array'
-
1
require 'data_objects/pooling'
-
1
require 'data_objects/connection'
-
1
require 'data_objects/uri'
-
1
require 'data_objects/transaction'
-
1
require 'data_objects/command'
-
1
require 'data_objects/result'
-
1
require 'data_objects/reader'
-
1
require 'data_objects/quoting'
-
1
require 'data_objects/extension'
-
1
require 'data_objects/error'
-
1
require 'data_objects/error/sql_error'
-
1
require 'data_objects/error/connection_error'
-
1
require 'data_objects/error/data_error'
-
1
require 'data_objects/error/integrity_error'
-
1
require 'data_objects/error/syntax_error'
-
1
require 'data_objects/error/transaction_error'
-
# This class has exists to represent binary data. This is mainly
-
# used by DataObjects. Binary data sometimes needs to be quoted differently
-
# than regular string data (even if the string is just plain ASCII).
-
1
module Extlib
-
1
class ByteArray < ::String; end
-
end
-
1
module DataObjects
-
# Abstract base class for adapter-specific Command subclasses
-
1
class Command
-
-
# The Connection on which the command will be run
-
1
attr_reader :connection
-
-
# Create a new Command object on the specified connection
-
1
def initialize(connection, text)
-
221
raise ArgumentError.new("+connection+ must be a DataObjects::Connection") unless DataObjects::Connection === connection
-
221
@connection, @text = connection, text
-
end
-
-
# Execute this command and return no dataset
-
1
def execute_non_query(*args)
-
raise NotImplementedError.new
-
end
-
-
# Execute this command and return a DataObjects::Reader for a dataset
-
1
def execute_reader(*args)
-
raise NotImplementedError.new
-
end
-
-
# Assign an array of types for the columns to be returned by this command
-
1
def set_types(column_types)
-
raise NotImplementedError.new
-
end
-
-
# Display the command text
-
1
def to_s
-
@text
-
end
-
-
1
private
-
-
# Escape a string of SQL with a set of arguments.
-
# The first argument is assumed to be the SQL to escape,
-
# the remaining arguments (if any) are assumed to be
-
# values to escape and interpolate.
-
#
-
# ==== Examples
-
# escape_sql("SELECT * FROM zoos")
-
# # => "SELECT * FROM zoos"
-
#
-
# escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
-
# # => "SELECT * FROM zoos WHERE name = `Dallas`"
-
#
-
# escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
-
# # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
-
#
-
# ==== Warning
-
# This method is meant mostly for adapters that don't support
-
# bind-parameters.
-
1
def escape_sql(args)
-
221
return @text if args.empty?
-
128
sql = @text.dup
-
128
vars = args.dup
-
-
128
replacements = 0
-
128
mismatch = false
-
-
128
sql.gsub!(/'[^']*'|"[^"]*"|`[^`]*`|\?/) do |x|
-
870
next x unless x == '?'
-
280
replacements += 1
-
280
if vars.empty?
-
mismatch = true
-
else
-
280
var = vars.shift
-
280
connection.quote_value(var)
-
end
-
end
-
-
128
if !vars.empty? || mismatch
-
raise ArgumentError, "Binding mismatch: #{args.size} for #{replacements}"
-
else
-
128
sql
-
end
-
end
-
-
end
-
-
end
-
1
begin
-
1
require 'fastthread'
-
rescue LoadError
-
end
-
-
1
module DataObjects
-
# An abstract connection to a DataObjects resource. The physical connection may be broken and re-established from time to time.
-
1
class Connection
-
-
1
include Logging
-
-
# Make a connection to the database using the DataObjects::URI given.
-
# Note that the physical connection may be delayed until the first command is issued, so success here doesn't necessarily mean you can connect.
-
1
def self.new(uri_s)
-
226
uri = DataObjects::URI::parse(uri_s)
-
-
226
case uri.scheme.to_sym
-
when :java
-
warn 'JNDI URLs (connection strings) are only for use with JRuby' unless RUBY_PLATFORM =~ /java/
-
-
driver = uri.query.delete('scheme')
-
driver = uri.query.delete('driver')
-
-
conn_uri = uri.to_s.gsub(/\?$/, '')
-
when :jdbc
-
warn 'JDBC URLs (connection strings) are only for use with JRuby' unless RUBY_PLATFORM =~ /java/
-
-
path = uri.subscheme
-
driver = if path.split(':').first == 'sqlite'
-
'sqlite3'
-
elsif path.split(':').first == 'postgresql'
-
'postgres'
-
else
-
path.split(':').first
-
end
-
-
conn_uri = uri_s # NOTE: for now, do not reformat this JDBC connection
-
# string -- or, in other words, do not let
-
# DataObjects::URI#to_s be called -- as it is not
-
# correctly handling JDBC URLs, and in doing so, causing
-
# java.sql.DriverManager.getConnection to throw a
-
# 'No suitable driver found for...' exception.
-
else
-
226
driver = uri.scheme
-
226
conn_uri = uri
-
end
-
-
# Exceptions to how a driver class is determined for a given URI
-
226
driver_class = if driver == 'sqlserver'
-
'SqlServer'
-
else
-
226
driver.capitalize
-
end
-
-
226
clazz = DataObjects.const_get(driver_class)::Connection
-
226
unless clazz.method_defined? :close
-
1
if (uri.scheme.to_sym == :java)
-
clazz.class_eval do
-
alias close dispose
-
end
-
else
-
1
clazz.class_eval do
-
1
include Pooling
-
1
alias close release
-
end
-
end
-
end
-
226
clazz.new(conn_uri)
-
end
-
-
# Ensure that all Connection subclasses handle pooling and logging uniformly.
-
# See also DataObjects::Pooling and DataObjects::Logger
-
1
def self.inherited(target)
-
1
target.class_eval do
-
-
# Allocate a Connection object from the pool, creating one if necessary. This method is active in Connection subclasses only.
-
1
def self.new(*args)
-
2
instance = allocate
-
2
instance.send(:initialize, *args)
-
2
instance
-
end
-
-
1
include Quoting
-
end
-
-
1
if driver_module_name = target.name.split('::')[-2]
-
1
driver_module = DataObjects::const_get(driver_module_name)
-
1
driver_module.class_eval <<-EOS, __FILE__, __LINE__
-
def self.logger
-
@logger
-
end
-
-
def self.logger=(logger)
-
@logger = logger
-
end
-
EOS
-
-
1
driver_module.logger = DataObjects::Logger.new(nil, :off)
-
end
-
end
-
-
#####################################################
-
# Standard API Definition
-
#####################################################
-
-
# Show the URI for this connection, without
-
# the password the connection was setup with
-
1
def to_s
-
@uri.to_s
-
end
-
-
1
def initialize(uri) #:nodoc:
-
raise NotImplementedError.new
-
end
-
-
1
def dispose #:nodoc:
-
raise NotImplementedError.new
-
end
-
-
# Create a Command object of the right subclass using the given text
-
1
def create_command(text)
-
221
self.class.concrete_command.new(self, text)
-
end
-
-
1
def extension
-
driver_namespace.const_get('Extension').new(self)
-
end
-
-
1
private
-
-
1
def driver_namespace
-
229
DataObjects::const_get(self.class.name.split('::')[-2])
-
end
-
-
1
def self.concrete_command
-
221
@concrete_command ||= DataObjects::const_get(self.name.split('::')[-2]).const_get('Command')
-
end
-
-
end
-
-
end
-
1
module DataObjects
-
1
class Error < StandardError
-
end
-
end
-
1
module DataObjects
-
1
class ConnectionError < SQLError
-
end
-
end
-
1
module DataObjects
-
1
class DataError < SQLError
-
end
-
end
-
1
module DataObjects
-
1
class IntegrityError < SQLError
-
end
-
end
-
1
module DataObjects
-
1
class SQLError < Error
-
-
1
attr_reader :message
-
1
attr_reader :code
-
1
attr_reader :sqlstate
-
1
attr_reader :query
-
1
attr_reader :uri
-
-
1
def initialize(message, code = nil, sqlstate = nil, query = nil, uri = nil)
-
@message = message
-
@code = code
-
@sqlstate = sqlstate
-
@query = query
-
@uri = uri
-
end
-
-
1
def to_s
-
"#{message} (code: #{code}, sql state: #{sqlstate}, query: #{query}, uri: #{uri})"
-
end
-
end
-
end
-
1
module DataObjects
-
1
class SyntaxError < SQLError
-
end
-
end
-
1
module DataObjects
-
1
class TransactionError < SQLError
-
end
-
end
-
1
module DataObjects
-
1
class Extension
-
-
1
attr_reader :connection
-
-
1
def initialize(connection)
-
@connection = connection
-
end
-
-
end
-
end
-
1
require "time" # httpdate
-
-
1
module DataObjects
-
-
1
module Logging
-
-
1
def log(message)
-
229
logger = driver_namespace.logger
-
229
if logger.level <= DataObjects::Logger::LEVELS[:debug]
-
message = "(%.6f) %s" % [message.duration / 1000000.0, message.query]
-
logger.debug message
-
end
-
end
-
-
end
-
-
1
class << self
-
# The global logger for DataObjects
-
1
attr_accessor :logger
-
end
-
-
# ==== Public DataObjects Logger API
-
#
-
# Logger taken from Merb :)
-
#
-
# To replace an existing logger with a new one:
-
# DataObjects::Logger.set_log(log{String, IO},level{Symbol, String})
-
#
-
# Available logging levels are
-
# DataObjects::Logger::{ Fatal, Error, Warn, Info, Debug }
-
#
-
# Logging via:
-
# DataObjects.logger.fatal(message<String>)
-
# DataObjects.logger.error(message<String>)
-
# DataObjects.logger.warn(message<String>)
-
# DataObjects.logger.info(message<String>)
-
# DataObjects.logger.debug(message<String>)
-
#
-
# Flush the buffer to
-
# DataObjects.logger.flush
-
#
-
# Remove the current log object
-
# DataObjects.logger.close
-
#
-
# ==== Private DataObjects Logger API
-
#
-
# To initialize the logger you create a new object, proxies to set_log.
-
# DataObjects::Logger.new(log{String, IO},level{Symbol, String})
-
#
-
# Logger will not create the file until something is actually logged
-
# This avoids file creation on DataObjects init when it creates the
-
# default logger.
-
1
class Logger
-
-
# Use asynchronous I/O?
-
1
attr_accessor :aio
-
# delimiter to use between message sections
-
1
attr_accessor :delimiter
-
# a symbol representing the log level from {:off, :fatal, :error, :warn, :info, :debug}
-
1
attr_reader :level
-
# Direct access to the buffer
-
1
attr_reader :buffer
-
# The name of the log file
-
1
attr_reader :log
-
-
1
Message = Struct.new(:query, :start, :duration)
-
-
#
-
# Ruby (standard) logger levels:
-
# off: absolutely nothing
-
# fatal: an unhandleable error that results in a program crash
-
# error: a handleable error condition
-
# warn: a warning
-
# info: generic (useful) information about system operation
-
# debug: low-level information for developers
-
#
-
# DataObjects::Logger::LEVELS[:off, :fatal, :error, :warn, :info, :debug]
-
1
LEVELS =
-
{
-
:off => 99999,
-
:fatal => 7,
-
:error => 6,
-
:warn => 4,
-
:info => 3,
-
:debug => 0
-
}
-
-
# Set the log level (use the level symbols as documented)
-
1
def level=(new_level)
-
1
@level = LEVELS[new_level.to_sym]
-
1
reset_methods(:close)
-
end
-
-
1
private
-
-
# The idea here is that instead of performing an 'if' conditional check on
-
# each logging we do it once when the log object is setup
-
1
def set_write_method
-
@log.instance_eval do
-
-
# Determine if asynchronous IO can be used
-
def aio?
-
@aio = !RUBY_PLATFORM.match(/java|mswin/) &&
-
!(@log == STDOUT) &&
-
@log.respond_to?(:write_nonblock)
-
end
-
-
# Define the write method based on if aio an be used
-
undef write_method if defined? write_method
-
if aio?
-
alias :write_method :write_nonblock
-
else
-
alias :write_method :write
-
end
-
end
-
end
-
-
1
def initialize_log(log)
-
1
close if @log # be sure that we don't leave open files laying around.
-
1
@log = log || "log/dm.log"
-
end
-
-
1
def reset_methods(o_or_c)
-
1
if o_or_c == :open
-
alias internal_push push_opened
-
1
elsif o_or_c == :close
-
1
alias internal_push push_closed
-
end
-
end
-
-
1
def push_opened(string)
-
message = Time.now.httpdate
-
message << delimiter
-
message << string
-
message << "\n" unless message[-1] == ?\n
-
@buffer << message
-
flush # Force a flush for now until we figure out where we want to use the buffering.
-
end
-
-
1
def push_closed(string)
-
unless @log.respond_to?(:write)
-
log = Pathname(@log)
-
log.dirname.mkpath
-
@log = log.open('a')
-
@log.sync = true
-
end
-
set_write_method
-
reset_methods(:open)
-
push(string)
-
end
-
-
1
alias internal_push push_closed
-
-
1
def prep_msg(message, level)
-
level << delimiter << message
-
end
-
-
1
public
-
-
# To initialize the logger you create a new object, proxies to set_log.
-
# DataObjects::Logger.new(log{String, IO},level{Symbol, String})
-
#
-
# @param log<IO,String> either an IO object or a name of a logfile.
-
# @param log_level<String> the message string to be logged
-
# @param delimiter<String> delimiter to use between message sections
-
# @param log_creation<Boolean> log that the file is being created
-
1
def initialize(*args)
-
1
set_log(*args)
-
end
-
-
# To replace an existing logger with a new one:
-
# DataObjects::Logger.set_log(log{String, IO},level{Symbol, String})
-
#
-
#
-
# @param log<IO,String> either an IO object or a name of a logfile.
-
# @param log_level<Symbol> a symbol representing the log level from
-
# {:off, :fatal, :error, :warn, :info, :debug}
-
# @param delimiter<String> delimiter to use between message sections
-
# @param log_creation<Boolean> log that the file is being created
-
1
def set_log(log, log_level = :off, delimiter = " ~ ", log_creation = false)
-
1
delimiter ||= " ~ "
-
-
1
if log_level && LEVELS[log_level.to_sym]
-
1
self.level = log_level.to_sym
-
else
-
self.level = :debug
-
end
-
-
1
@buffer = []
-
1
@delimiter = delimiter
-
-
1
initialize_log(log)
-
-
1
DataObjects.logger = self
-
-
1
self.info("Logfile created") if log_creation
-
end
-
-
# Flush the entire buffer to the log object.
-
# DataObjects.logger.flush
-
#
-
1
def flush
-
return unless @buffer.size > 0
-
@log.write_method(@buffer.slice!(0..-1).join)
-
end
-
-
# Close and remove the current log object.
-
# DataObjects.logger.close
-
#
-
1
def close
-
flush
-
@log.close if @log.respond_to?(:close)
-
@log = nil
-
end
-
-
# Appends a string and log level to logger's buffer.
-
-
#
-
# Note that the string is discarded if the string's log level less than the
-
# logger's log level.
-
#
-
# Note that if the logger is aio capable then the logger will use
-
# non-blocking asynchronous writes.
-
#
-
# @param level<Fixnum> the logging level as an integer
-
# @param string<String> the message string to be logged
-
1
def push(string)
-
internal_push(string)
-
end
-
1
alias << push
-
-
# Generate the following logging methods for DataObjects.logger as described
-
# in the API:
-
# :fatal, :error, :warn, :info, :debug
-
# :off only gets an off? method
-
1
LEVELS.each_pair do |name, number|
-
6
unless name.to_sym == :off
-
5
class_eval <<-EOS, __FILE__, __LINE__
-
# DOC
-
def #{name}(message)
-
self.<<( prep_msg(message, "#{name}") ) if #{name}?
-
end
-
EOS
-
end
-
-
6
class_eval <<-EOS, __FILE__, __LINE__
-
# DOC
-
def #{name}?
-
#{number} >= level
-
end
-
EOS
-
end
-
-
end # class Logger
-
end # module DataObjects
-
1
require 'set'
-
1
require 'thread'
-
-
1
module DataObjects
-
-
1
def self.exiting= bool
-
if bool && DataObjects.const_defined?('Pooling')
-
if DataObjects::Pooling.scavenger?
-
DataObjects::Pooling.scavenger.wakeup
-
end
-
end
-
@exiting = true
-
end
-
-
1
def self.exiting
-
return @exiting if defined?(@exiting)
-
@exiting = false
-
end
-
-
# ==== Notes
-
# Provides pooling support to class it got included in.
-
#
-
# Pooling of objects is a faster way of aquiring instances
-
# of objects compared to regular allocation and initialization
-
# because instances are keeped in memory reused.
-
#
-
# Classes that include Pooling module have re-defined new
-
# method that returns instances acquired from pool.
-
#
-
# Term resource is used for any type of poolable objects
-
# and should NOT be thought as DataMapper Resource or
-
# ActiveResource resource and such.
-
#
-
# In Data Objects connections are pooled so that it is
-
# unnecessary to allocate and initialize connection object
-
# each time connection is needed, like per request in a
-
# web application.
-
#
-
# Pool obviously has to be thread safe because state of
-
# object is reset when it is released.
-
1
module Pooling
-
-
1
def self.scavenger?
-
1
defined?(@scavenger) && !@scavenger.nil? && @scavenger.alive?
-
end
-
-
1
def self.scavenger
-
1
unless scavenger?
-
1
@scavenger = Thread.new do
-
1
running = true
-
1
while running do
-
# Sleep before we actually start doing anything.
-
# Otherwise we might clean up something we just made
-
1
sleep(scavenger_interval)
-
-
lock.synchronize do
-
pools.each do |pool|
-
# This is a useful check, but non-essential, and right now it breaks lots of stuff.
-
# if pool.expired?
-
pool.lock.synchronize do
-
if pool.expired?
-
pool.dispose
-
end
-
end
-
# end
-
end
-
-
# The pool is empty, we stop the scavenger
-
# It wil be restarted if new resources are added again
-
if pools.empty?
-
running = false
-
end
-
end
-
end # loop
-
end
-
end
-
-
1
@scavenger.priority = -10
-
1
@scavenger
-
end
-
-
1
def self.pools
-
1
@pools ||= Set.new
-
end
-
-
1
def self.append_pool(pool)
-
1
lock.synchronize do
-
1
pools << pool
-
end
-
1
DataObjects::Pooling.scavenger
-
end
-
-
1
def self.lock
-
2
@lock ||= Mutex.new
-
end
-
-
1
class InvalidResourceError < StandardError
-
end
-
-
1
def self.included(target)
-
1
lock.synchronize do
-
1
unless target.respond_to? :__pools
-
1
target.class_eval do
-
1
class << self
-
1
alias __new new
-
end
-
-
1
@__pools = {}
-
1
@__pool_lock = Mutex.new
-
1
@__pool_wait = ConditionVariable.new
-
-
1
def self.__pool_lock
-
453
@__pool_lock
-
end
-
-
1
def self.__pool_wait
-
226
@__pool_wait
-
end
-
-
1
def self.new(*args)
-
227
(@__pools[args] ||= __pool_lock.synchronize { Pool.new(self.pool_size, self, args) }).new
-
end
-
-
1
def self.__pools
-
@__pools
-
end
-
-
1
def self.pool_size
-
1
8
-
end
-
end
-
end
-
end
-
end
-
-
1
def release
-
226
@__pool.release(self) unless @__pool.nil?
-
end
-
-
1
def detach
-
@__pool.delete(self) unless @__pool.nil?
-
end
-
-
1
class Pool
-
1
attr_reader :available
-
1
attr_reader :used
-
-
1
def initialize(max_size, resource, args)
-
1
raise ArgumentError.new("+max_size+ should be a Fixnum but was #{max_size.inspect}") unless Fixnum === max_size
-
1
raise ArgumentError.new("+resource+ should be a Class but was #{resource.inspect}") unless Class === resource
-
-
1
@max_size = max_size
-
1
@resource = resource
-
1
@args = args
-
-
1
@available = []
-
1
@used = {}
-
1
DataObjects::Pooling.append_pool(self)
-
end
-
-
1
def lock
-
452
@resource.__pool_lock
-
end
-
-
1
def wait
-
226
@resource.__pool_wait
-
end
-
-
1
def scavenge_interval
-
@resource.scavenge_interval
-
end
-
-
1
def new
-
226
instance = nil
-
begin
-
226
lock.synchronize do
-
226
if @available.size > 0
-
224
instance = @available.pop
-
224
@used[instance.object_id] = instance
-
2
elsif @used.size < @max_size
-
2
instance = @resource.__new(*@args)
-
2
raise InvalidResourceError.new("#{@resource} constructor created a nil object") if instance.nil?
-
2
raise InvalidResourceError.new("#{instance} is already part of the pool") if @used.include? instance
-
2
instance.instance_variable_set(:@__pool, self)
-
2
instance.instance_variable_set(:@__allocated_in_pool, Time.now)
-
2
@used[instance.object_id] = instance
-
else
-
# Wait for another thread to release an instance.
-
# If we exhaust the pool and don't release the active instance,
-
# we'll wait here forever, so it's *very* important to always
-
# release your services and *never* exhaust the pool within
-
# a single thread.
-
wait.wait(lock)
-
end
-
end
-
226
end until instance
-
226
instance
-
end
-
-
1
def release(instance)
-
226
lock.synchronize do
-
226
instance.instance_variable_set(:@__allocated_in_pool, Time.now)
-
226
@used.delete(instance.object_id)
-
226
@available.push(instance) unless @available.include?(instance)
-
226
wait.signal
-
end
-
nil
-
end
-
-
1
def delete(instance)
-
lock.synchronize do
-
instance.instance_variable_set(:@__pool, nil)
-
@used.delete(instance.object_id)
-
wait.signal
-
end
-
nil
-
end
-
-
1
def size
-
@used.size + @available.size
-
end
-
1
alias length size
-
-
1
def inspect
-
"#<DataObjects::Pooling::Pool<#{@resource.name}> available=#{@available.size} used=#{@used.size} size=#{@max_size}>"
-
end
-
-
1
def flush!
-
@available.pop.dispose until @available.empty?
-
end
-
-
1
def dispose
-
flush!
-
@resource.__pools.delete(@args)
-
!DataObjects::Pooling.pools.delete?(self).nil?
-
end
-
-
1
def expired?
-
@available.each do |instance|
-
if DataObjects.exiting || instance.instance_variable_get(:@__allocated_in_pool) + DataObjects::Pooling.scavenger_interval <= (Time.now + 0.02)
-
instance.dispose
-
@available.delete(instance)
-
end
-
end
-
size == 0
-
end
-
-
end
-
-
1
def self.scavenger_interval
-
1
60
-
end
-
end # module Pooling
-
end # module DataObjects
-
1
module DataObjects
-
-
1
module Quoting
-
-
# Quote a value of any of the recognised data types
-
1
def quote_value(value)
-
280
return 'NULL' if value.nil?
-
-
280
case value
-
97
when Numeric then quote_numeric(value)
-
when ::Extlib::ByteArray then quote_byte_array(value)
-
171
when String then quote_string(value)
-
when Time then quote_time(value)
-
when DateTime then quote_datetime(value)
-
12
when Date then quote_date(value)
-
when TrueClass, FalseClass then quote_boolean(value)
-
when Array then quote_array(value)
-
when Range then quote_range(value)
-
when Symbol then quote_symbol(value)
-
when Regexp then quote_regexp(value)
-
when Class then quote_class(value)
-
else
-
if value.respond_to?(:to_sql)
-
value.to_sql
-
else
-
raise "Don't know how to quote #{value.class} objects (#{value.inspect})"
-
end
-
end
-
end
-
-
# Convert the Symbol to a String and quote that
-
1
def quote_symbol(value)
-
quote_string(value.to_s)
-
end
-
-
# Convert the Numeric to a String and quote that
-
1
def quote_numeric(value)
-
97
value.to_s
-
end
-
-
# Quote a String for SQL by doubling any embedded single-quote characters
-
1
def quote_string(value)
-
"'#{value.gsub("'", "''")}'"
-
end
-
-
# Quote a class by quoting its name
-
1
def quote_class(value)
-
quote_string(value.name)
-
end
-
-
# Convert a Time to standard YMDHMS format (with microseconds if necessary)
-
1
def quote_time(value)
-
offset = value.utc_offset
-
if offset >= 0
-
offset_string = "+#{sprintf("%02d", offset / 3600)}:#{sprintf("%02d", (offset % 3600) / 60)}"
-
elsif offset < 0
-
offset_string = "-#{sprintf("%02d", -offset / 3600)}:#{sprintf("%02d", (-offset % 3600) / 60)}"
-
end
-
"'#{value.strftime('%Y-%m-%dT%H:%M:%S')}" << (value.usec > 0 ? ".#{value.usec.to_s.rjust(6, '0')}" : "") << offset_string << "'"
-
end
-
-
# Quote a DateTime by relying on it's own to_s conversion
-
1
def quote_datetime(value)
-
"'#{value.dup}'"
-
end
-
-
# Convert a Date to standard YMD format
-
1
def quote_date(value)
-
12
"'#{value.strftime("%Y-%m-%d")}'"
-
end
-
-
# Quote true, false as the strings TRUE, FALSE
-
1
def quote_boolean(value)
-
value.to_s.upcase
-
end
-
-
# Quote an array as a list of quoted values
-
1
def quote_array(value)
-
"(#{value.map { |entry| quote_value(entry) }.join(', ')})"
-
end
-
-
# Quote a range by joining the quoted end-point values with AND.
-
# It's not clear whether or when this is a useful or correct thing to do.
-
1
def quote_range(value)
-
"#{quote_value(value.first)} AND #{quote_value(value.last)}"
-
end
-
-
# Quote a Regex using its string value. Note that there's no attempt to make a valid SQL "LIKE" string.
-
1
def quote_regexp(value)
-
quote_string(value.source)
-
end
-
-
1
def quote_byte_array(value)
-
quote_string(value)
-
end
-
-
end
-
-
end
-
1
module DataObjects
-
# Abstract class to read rows from a query result
-
1
class Reader
-
-
1
include Enumerable
-
-
# Return the array of field names
-
1
def fields
-
raise NotImplementedError.new
-
end
-
-
# Return the array of field values for the current row. Not legal after next! has returned false or before it's been called
-
1
def values
-
raise NotImplementedError.new
-
end
-
-
# Close the reader discarding any unread results.
-
1
def close
-
raise NotImplementedError.new
-
end
-
-
# Discard the current row (if any) and read the next one (returning true), or return nil if there is no further row.
-
1
def next!
-
raise NotImplementedError.new
-
end
-
-
# Return the number of fields in the result set.
-
1
def field_count
-
raise NotImplementedError.new
-
end
-
-
# Yield each row to the given block as a Hash
-
1
def each
-
begin
-
while next!
-
row = {}
-
fields.each_with_index { |field, index| row[field] = values[index] }
-
yield row
-
end
-
ensure
-
close
-
end
-
self
-
end
-
-
end
-
end
-
1
module DataObjects
-
# The Result class is returned from Connection#execute_non_query.
-
1
class Result
-
# The ID of a row inserted by the Command
-
1
attr_accessor :insert_id
-
# The number of rows affected by the Command
-
1
attr_accessor :affected_rows
-
-
# Create a new Result. Used internally in the adapters.
-
1
def initialize(command, affected_rows, insert_id = nil)
-
55
@command, @affected_rows, @insert_id = command, affected_rows, insert_id
-
end
-
-
# Return the number of affected rows
-
1
def to_i
-
@affected_rows
-
end
-
end
-
end
-
1
require 'socket'
-
1
require 'digest'
-
1
require 'digest/sha2'
-
-
1
module DataObjects
-
-
1
class Transaction
-
-
# The local host name. Do not attempt to resolve in DNS to prevent potentially long delay
-
1
HOST = "#{Socket::gethostname}" rescue "localhost"
-
1
@@counter = 0
-
-
# The connection object allocated for this transaction
-
1
attr_reader :connection
-
# A unique ID for this transaction
-
1
attr_reader :id
-
-
# Instantiate the Transaction subclass that's appropriate for this uri scheme
-
1
def self.create_for_uri(uri)
-
uri = uri.is_a?(String) ? URI::parse(uri) : uri
-
DataObjects.const_get(uri.scheme.capitalize)::Transaction.new(uri)
-
end
-
-
#
-
# Creates a Transaction bound to a connection for the given DataObjects::URI
-
#
-
1
def initialize(uri, connection = nil)
-
@connection = connection || DataObjects::Connection.new(uri)
-
# PostgreSQL can't handle the full 64 bytes. This should be enough for everyone.
-
@id = Digest::SHA256.hexdigest("#{HOST}:#{$$}:#{Time.now.to_f}:#{@@counter += 1}")[0..-2]
-
end
-
-
# Close the connection for this Transaction
-
1
def close
-
@connection.close
-
end
-
-
1
def begin
-
run "BEGIN"
-
end
-
-
1
def commit
-
run "COMMIT"
-
end
-
-
1
def rollback
-
run "ROLLBACK"
-
end
-
-
1
def prepare; not_implemented; end;
-
1
def begin_prepared; not_implemented; end;
-
1
def commit_prepared; not_implemented; end;
-
1
def rollback_prepared; not_implemented; end;
-
1
def prepare; not_implemented; end;
-
-
1
protected
-
1
def run(cmd)
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
private
-
1
def not_implemented
-
raise NotImplementedError
-
end
-
end # class Transaction
-
-
1
class SavePoint < Transaction
-
# We don't bounce through DO::<Adapter/scheme>::SavePoint because there
-
# doesn't appear to be any custom SQL to support this.
-
1
def self.create_for_uri(uri, connection)
-
uri = uri.is_a?(String) ? URI::parse(uri) : uri
-
DataObjects::SavePoint.new(uri, connection)
-
end
-
-
# SavePoints can only occur in the context of a Transaction, thus they
-
# re-use TXN's connection (which was acquired from the connection pool
-
# legitimately via DO::Connection.new). We no-op #close in SP because
-
# calling DO::Connection#close will release the connection back into the
-
# pool (before the top-level Transaction might be done with it).
-
1
def close
-
# no-op
-
end
-
-
1
def begin
-
run %{SAVEPOINT "#{@id}"}
-
end
-
-
1
def commit
-
run %{RELEASE SAVEPOINT "#{@id}"}
-
end
-
-
1
def rollback
-
run %{ROLLBACK TO SAVEPOINT "#{@id}"}
-
end
-
end # class SavePoint
-
-
end
-
1
require 'addressable/uri'
-
-
1
module DataObjects
-
-
# A DataObjects URI is of the form scheme://user:password@host:port/path#fragment
-
#
-
# The elements are all optional except scheme and path:
-
# scheme:: The name of a DBMS for which you have a do_\<scheme\> adapter gem installed. If scheme is *jdbc*, the actual DBMS is in the _path_ followed by a colon.
-
# user:: The name of the user to authenticate to the database
-
# password:: The password to use in authentication
-
# host:: The domain name (defaulting to localhost) where the database is available
-
# port:: The TCP/IP port number to use for the connection
-
# path:: The name or path to the database
-
# query:: Parameters for the connection, for example encoding=utf8
-
# fragment:: Not currently known to be in use, but available to the adapters
-
1
class URI
-
1
attr_reader :scheme, :subscheme, :user, :password, :host, :port, :path, :query, :fragment
-
-
# Make a DataObjects::URI object by parsing a string. Simply delegates to Addressable::URI::parse.
-
1
def self.parse(uri)
-
226
return uri if uri.kind_of?(self)
-
-
if uri.kind_of?(Addressable::URI)
-
scheme = uri.scheme
-
else
-
if uri[0,4] == 'jdbc'
-
scheme = uri[0,4]
-
uri = Addressable::URI::parse(uri[5, uri.length])
-
subscheme = uri.scheme
-
else
-
uri = Addressable::URI::parse(uri)
-
scheme = uri.scheme
-
subscheme = nil
-
end
-
end
-
-
self.new(
-
:scheme => scheme,
-
:subscheme => subscheme,
-
:user => uri.user,
-
:password => uri.password,
-
:host => uri.host,
-
:port => uri.port,
-
:path => uri.path,
-
:query => uri.query_values,
-
:fragment => uri.fragment,
-
:relative => !!uri.to_s.index('//') # basic (naive) check for relativity / opaqueness
-
)
-
end
-
-
1
def initialize(*args)
-
1
if (component = args.first).kind_of?(Hash)
-
1
@scheme = component[:scheme]
-
1
@subscheme = component[:subscheme]
-
1
@user = component[:user]
-
1
@password = component[:password]
-
1
@host = component[:host]
-
1
@port = component[:port]
-
1
@path = component[:path]
-
1
@query = component[:query]
-
1
@fragment = component[:fragment]
-
1
@relative = component[:relative]
-
elsif args.size > 1
-
warn "DataObjects::URI.new with arguments is deprecated, use a Hash of URI components (#{caller.first})"
-
@scheme, @user, @password, @host, @port, @path, @query, @fragment = *args
-
else
-
raise ArgumentError, "argument should be a Hash of URI components, was: #{args.inspect}"
-
end
-
end
-
-
1
def opaque?
-
!@relative
-
end
-
-
1
def relative?
-
676
@relative
-
end
-
-
# Display this URI object as a string
-
1
def to_s
-
676
string = ""
-
676
string << "#{scheme}:" if scheme
-
676
string << "#{subscheme}:" if subscheme
-
676
string << '//' if relative?
-
676
if user
-
string << "#{user}"
-
string << "@"
-
end
-
676
string << "#{host}" if host
-
676
string << ":#{port}" if port
-
676
string << path.to_s
-
676
if query
-
string << "?" << query.map do |key, value|
-
6084
"#{key}=#{value}"
-
676
end.join("&")
-
end
-
676
string << "##{fragment}" if fragment
-
676
string
-
end
-
-
# Compare this URI to another for hashing
-
1
def eql?(other)
-
225
to_s.eql?(other.to_s)
-
end
-
-
# Hash this URI
-
1
def hash
-
226
to_s.hash
-
end
-
-
end
-
end
-
# This is here to remove DataObject's dependency on Extlib.
-
-
1
module DataObjects
-
# @param name<String> The name of the constant to get, e.g. "Merb::Router".
-
#
-
# @return <Object> The constant corresponding to the name.
-
1
def self.full_const_get(name)
-
list = name.split("::")
-
list.shift if list.first.nil? || list.first.strip.empty?
-
obj = ::Object
-
list.each do |x|
-
# This is required because const_get tries to look for constants in the
-
# ancestor chain, but we only want constants that are HERE
-
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
-
end
-
obj
-
end
-
end
-
1
module DataObjects
-
1
VERSION = '0.10.17'
-
end
-
1
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
-
1
require 'database_cleaner/configuration'
-
-
1
module DatabaseCleaner
-
1
def self.can_detect_orm?
-
DatabaseCleaner::Base.autodetect_orm
-
end
-
end
-
1
require 'database_cleaner/null_strategy'
-
1
module DatabaseCleaner
-
1
class Base
-
1
include Comparable
-
-
1
def <=>(other)
-
(self.orm <=> other.orm) == 0 ? self.db <=> other.db : self.orm <=> other.orm
-
end
-
-
1
def initialize(desired_orm = nil,opts = {})
-
2
if [:autodetect, nil, "autodetect"].include?(desired_orm)
-
1
autodetect
-
else
-
1
self.orm = desired_orm
-
end
-
2
self.db = opts[:connection] || opts[:model] if opts.has_key?(:connection) || opts.has_key?(:model)
-
2
set_default_orm_strategy
-
end
-
-
1
def db=(desired_db)
-
self.strategy_db = desired_db
-
@db = desired_db
-
end
-
-
1
def strategy_db=(desired_db)
-
if strategy.respond_to? :db=
-
strategy.db = desired_db
-
elsif desired_db!= :default
-
raise ArgumentError, "You must provide a strategy object that supports non default databases when you specify a database"
-
end
-
end
-
-
1
def db
-
4
@db ||= :default
-
end
-
-
1
def create_strategy(*args)
-
4
strategy, *strategy_args = args
-
4
orm_strategy(strategy).new(*strategy_args)
-
end
-
-
1
def clean_with(*args)
-
1
strategy = create_strategy(*args)
-
1
set_strategy_db strategy, self.db
-
-
1
strategy.clean
-
1
strategy
-
end
-
-
1
alias clean_with! clean_with
-
-
1
def set_strategy_db(strategy, desired_db)
-
4
if strategy.respond_to? :db=
-
4
strategy.db = desired_db
-
elsif desired_db != :default
-
raise ArgumentError, "You must provide a strategy object that supports non default databases when you specify a database"
-
end
-
end
-
-
1
def strategy=(args)
-
3
strategy, *strategy_args = args
-
3
if strategy.is_a?(Symbol)
-
3
@strategy = create_strategy(*args)
-
elsif strategy_args.empty?
-
@strategy = strategy
-
else
-
raise ArgumentError, "You must provide a strategy object, or a symbol for a known strategy along with initialization params."
-
end
-
-
3
set_strategy_db @strategy, self.db
-
-
3
@strategy
-
end
-
-
1
def strategy
-
28
@strategy ||= NullStrategy
-
end
-
-
1
def orm=(desired_orm)
-
1
@orm = desired_orm.to_sym
-
end
-
-
1
def orm
-
11
@orm || autodetect
-
end
-
-
1
def start
-
14
strategy.start
-
end
-
-
1
def clean
-
14
strategy.clean
-
end
-
-
1
alias clean! clean
-
-
1
def cleaning(&block)
-
strategy.cleaning(&block)
-
end
-
-
1
def auto_detected?
-
!!@autodetected
-
end
-
-
1
def autodetect_orm
-
1
if defined? ::ActiveRecord
-
:active_record
-
1
elsif defined? ::DataMapper
-
1
:data_mapper
-
elsif defined? ::MongoMapper
-
:mongo_mapper
-
elsif defined? ::Mongoid
-
:mongoid
-
elsif defined? ::CouchPotato
-
:couch_potato
-
elsif defined? ::Sequel
-
:sequel
-
elsif defined? ::Moped
-
:moped
-
elsif defined? ::Ohm
-
:ohm
-
elsif defined? ::Redis
-
:redis
-
elsif defined? ::Neo4j
-
:neo4j
-
end
-
end
-
-
1
private
-
-
1
def orm_module
-
4
::DatabaseCleaner.orm_module(orm)
-
end
-
-
1
def orm_strategy(strategy)
-
4
require "database_cleaner/#{orm.to_s}/#{strategy.to_s}"
-
4
orm_module.const_get(strategy.to_s.capitalize)
-
rescue LoadError
-
if orm_module.respond_to? :available_strategies
-
raise UnknownStrategySpecified, "The '#{strategy}' strategy does not exist for the #{orm} ORM! Available strategies: #{orm_module.available_strategies.join(', ')}"
-
else
-
raise UnknownStrategySpecified, "The '#{strategy}' strategy does not exist for the #{orm} ORM!"
-
end
-
end
-
-
1
def autodetect
-
1
@autodetected = true
-
-
@orm ||= autodetect_orm ||
-
1
raise(NoORMDetected, "No known ORM was detected! Is ActiveRecord, DataMapper, Sequel, MongoMapper, Mongoid, Moped, or CouchPotato, Redis or Ohm loaded?")
-
end
-
-
1
def set_default_orm_strategy
-
2
case orm
-
when :active_record, :data_mapper, :sequel
-
2
self.strategy = :transaction
-
when :mongo_mapper, :mongoid, :couch_potato, :moped, :ohm, :redis
-
self.strategy = :truncation
-
when :neo4j
-
self.strategy = :transaction
-
end
-
end
-
end
-
end
-
1
require 'database_cleaner/base'
-
-
1
module DatabaseCleaner
-
-
1
class NoORMDetected < StandardError; end
-
1
class UnknownStrategySpecified < ArgumentError; end
-
-
1
class << self
-
1
def init_cleaners
-
1
@cleaners ||= {}
-
# ghetto ordered hash.. maintains 1.8 compat and old API
-
1
@connections ||= []
-
end
-
-
1
def [](orm,opts = {})
-
raise NoORMDetected unless orm
-
init_cleaners
-
# TODO: deprecate
-
# this method conflates creation with lookup. Both a command and a query. Yuck.
-
if @cleaners.has_key? [orm, opts]
-
@cleaners[[orm, opts]]
-
else
-
add_cleaner(orm, opts)
-
end
-
end
-
-
1
def add_cleaner(orm,opts = {})
-
1
init_cleaners
-
1
cleaner = DatabaseCleaner::Base.new(orm,opts)
-
1
@cleaners[[orm, opts]] = cleaner
-
1
@connections << cleaner
-
1
cleaner
-
end
-
-
1
def app_root=(desired_root)
-
@app_root = desired_root
-
end
-
-
1
def app_root
-
@app_root ||= Dir.pwd
-
end
-
-
1
def connections
-
# double yuck.. can't wait to deprecate this whole class...
-
31
unless defined?(@cleaners) && @cleaners
-
1
autodetected = ::DatabaseCleaner::Base.new
-
1
add_cleaner(autodetected.orm)
-
end
-
31
@connections
-
end
-
-
1
def logger=(log_source)
-
@logger = log_source
-
end
-
-
1
def logger
-
return @logger if @logger
-
-
@logger = Logger.new(STDOUT)
-
@logger.level = Logger::ERROR
-
@logger
-
end
-
-
1
def strategy=(stratagem)
-
2
connections.each { |connect| connect.strategy = stratagem }
-
1
remove_duplicates
-
end
-
-
1
def orm=(orm)
-
connections.each { |connect| connect.orm = orm }
-
remove_duplicates
-
end
-
-
1
def start
-
28
connections.each { |connection| connection.start }
-
end
-
-
1
def clean
-
28
connections.each { |connection| connection.clean }
-
end
-
-
1
alias clean! clean
-
-
1
def cleaning(&inner_block)
-
connections.inject(inner_block) do |curr_block, connection|
-
proc { connection.cleaning(&curr_block) }
-
end.call
-
end
-
-
1
def clean_with(*args)
-
2
connections.each { |connection| connection.clean_with(*args) }
-
end
-
-
1
alias clean_with! clean_with
-
-
1
def remove_duplicates
-
1
temp = []
-
1
connections.each do |connect|
-
1
temp.push connect unless temp.include? connect
-
end
-
1
@connections = temp
-
end
-
-
1
def orm_module(symbol)
-
4
case symbol
-
when :active_record
-
DatabaseCleaner::ActiveRecord
-
when :data_mapper
-
4
DatabaseCleaner::DataMapper
-
when :mongo
-
DatabaseCleaner::Mongo
-
when :mongoid
-
DatabaseCleaner::Mongoid
-
when :mongo_mapper
-
DatabaseCleaner::MongoMapper
-
when :moped
-
DatabaseCleaner::Moped
-
when :couch_potato
-
DatabaseCleaner::CouchPotato
-
when :sequel
-
DatabaseCleaner::Sequel
-
when :ohm
-
DatabaseCleaner::Ohm
-
when :redis
-
DatabaseCleaner::Redis
-
when :neo4j
-
DatabaseCleaner::Neo4j
-
end
-
end
-
end
-
end
-
1
require 'database_cleaner/generic/base'
-
1
module DatabaseCleaner
-
1
module DataMapper
-
1
def self.available_strategies
-
%w[truncation transaction]
-
end
-
-
1
module Base
-
1
include ::DatabaseCleaner::Generic::Base
-
-
1
def db=(desired_db)
-
4
@db = desired_db
-
end
-
-
1
def db
-
15
@db ||= :default
-
end
-
-
end
-
end
-
end
-
1
require 'database_cleaner/data_mapper/base'
-
1
require 'database_cleaner/generic/transaction'
-
-
1
module DatabaseCleaner::DataMapper
-
1
class Transaction
-
1
include ::DatabaseCleaner::DataMapper::Base
-
1
include ::DatabaseCleaner::Generic::Transaction
-
-
1
def start(repository = self.db)
-
::DataMapper.repository(repository) do |r|
-
transaction = DataMapper::Transaction.new(r)
-
transaction.begin
-
r.adapter.push_transaction(transaction)
-
end
-
end
-
-
1
def clean(repository = self.db)
-
::DataMapper.repository(repository) do |r|
-
adapter = r.adapter
-
while adapter.current_transaction
-
adapter.current_transaction.rollback
-
adapter.pop_transaction
-
end
-
end
-
end
-
-
end
-
end
-
1
require "database_cleaner/generic/truncation"
-
1
require 'database_cleaner/data_mapper/base'
-
-
1
module DataMapper
-
1
module Adapters
-
-
1
class DataObjectsAdapter
-
-
1
def storage_names(repository = :default)
-
raise NotImplementedError
-
end
-
-
1
def truncate_tables(table_names)
-
table_names.each do |table_name|
-
truncate_table table_name
-
end
-
end
-
-
end
-
-
1
class MysqlAdapter < DataObjectsAdapter
-
-
# taken from http://github.com/godfat/dm-mapping/tree/master
-
1
def storage_names(repository = :default)
-
select 'SHOW TABLES'
-
end
-
-
1
def truncate_table(table_name)
-
execute("TRUNCATE TABLE #{quote_name(table_name)};")
-
end
-
-
# copied from activerecord
-
1
def disable_referential_integrity
-
old = select("SELECT @@FOREIGN_KEY_CHECKS;")
-
begin
-
execute("SET FOREIGN_KEY_CHECKS = 0;")
-
yield
-
ensure
-
execute("SET FOREIGN_KEY_CHECKS = ?", *old)
-
end
-
end
-
-
end
-
-
1
module SqliteAdapterMethods
-
-
# taken from http://github.com/godfat/dm-mapping/tree/master
-
1
def storage_names(repository = :default)
-
# activerecord-2.1.0/lib/active_record/connection_adapters/sqlite_adapter.rb: 177
-
sql = <<-SQL
-
SELECT name
-
FROM sqlite_master
-
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
-
SQL
-
# activerecord-2.1.0/lib/active_record/connection_adapters/sqlite_adapter.rb: 181
-
select(sql)
-
end
-
-
1
def truncate_table(table_name)
-
execute("DELETE FROM #{quote_name(table_name)};")
-
if uses_sequence?
-
execute("DELETE FROM sqlite_sequence where name = '#{table_name}';")
-
end
-
end
-
-
# this is a no-op copied from activerecord
-
# i didn't find out if/how this is possible
-
# activerecord also doesn't do more here
-
1
def disable_referential_integrity
-
yield
-
end
-
-
1
private
-
-
# Returns a boolean indicating if the SQLite database is using the sqlite_sequence table.
-
1
def uses_sequence?
-
sql = <<-SQL
-
SELECT name FROM sqlite_master
-
WHERE type='table' AND name='sqlite_sequence'
-
SQL
-
select(sql).first
-
end
-
end
-
-
2
class SqliteAdapter; include SqliteAdapterMethods; end
-
2
class Sqlite3Adapter; include SqliteAdapterMethods; end
-
-
# FIXME
-
# i don't know if this works
-
# i basically just copied activerecord code to get a rough idea what they do.
-
# i don't have postgres available, so i won't be the one to write this.
-
# maybe codes below gets some postgres/datamapper user going, though.
-
1
class PostgresAdapter < DataObjectsAdapter
-
-
# taken from http://github.com/godfat/dm-mapping/tree/master
-
1
def storage_names(repository = :default)
-
15
sql = <<-SQL
-
SELECT table_name FROM "information_schema"."tables"
-
WHERE table_schema = current_schema() and table_type = 'BASE TABLE'
-
SQL
-
15
select(sql)
-
end
-
-
1
def truncate_table(table_name)
-
execute("TRUNCATE TABLE #{quote_name(table_name)} RESTART IDENTITY CASCADE;")
-
end
-
-
# override to use a single statement
-
1
def truncate_tables(table_names)
-
90
quoted_names = table_names.collect { |n| quote_name(n) }.join(', ')
-
15
execute("TRUNCATE TABLE #{quoted_names} RESTART IDENTITY;")
-
end
-
-
# FIXME
-
# copied from activerecord
-
1
def supports_disable_referential_integrity?
-
30
version = select("SHOW server_version")[0][0].split('.')
-
30
(version[0].to_i >= 8 && version[1].to_i >= 1) ? true : false
-
rescue
-
return false
-
end
-
-
# FIXME
-
# copied unchanged from activerecord
-
1
def disable_referential_integrity(repository = :default)
-
15
if supports_disable_referential_integrity? then
-
execute(storage_names(repository).collect do |name|
-
"ALTER TABLE #{quote_name(name)} DISABLE TRIGGER ALL"
-
end.join(";"))
-
end
-
15
yield
-
ensure
-
15
if supports_disable_referential_integrity? then
-
execute(storage_names(repository).collect do |name|
-
"ALTER TABLE #{quote_name(name)} ENABLE TRIGGER ALL"
-
end.join(";"))
-
end
-
end
-
-
end
-
-
end
-
end
-
-
-
1
module DatabaseCleaner
-
1
module DataMapper
-
1
class Truncation
-
1
include ::DatabaseCleaner::DataMapper::Base
-
1
include ::DatabaseCleaner::Generic::Truncation
-
-
1
def clean(repository = self.db)
-
15
adapter = ::DataMapper.repository(repository).adapter
-
15
adapter.disable_referential_integrity do
-
15
adapter.truncate_tables(tables_to_truncate(repository))
-
end
-
end
-
-
1
private
-
-
1
def tables_to_truncate(repository = self.db)
-
15
(@only || ::DataMapper.repository(repository).adapter.storage_names(repository)) - @tables_to_exclude
-
end
-
-
# overwritten
-
1
def migration_storage_names
-
2
%w[migration_info]
-
end
-
-
end
-
end
-
end
-
1
module ::DatabaseCleaner
-
1
module Generic
-
1
module Base
-
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
def db
-
:default
-
end
-
-
1
def cleaning(&block)
-
begin
-
start
-
yield
-
ensure
-
clean
-
end
-
end
-
-
1
module ClassMethods
-
1
def available_strategies
-
%W[]
-
end
-
end
-
end
-
end
-
end
-
1
module DatabaseCleaner
-
1
module Generic
-
1
module Transaction
-
1
def initialize(opts = {})
-
2
if !opts.empty?
-
raise ArgumentError, "Options are not available for transaction strategies."
-
end
-
end
-
end
-
end
-
end
-
1
module DatabaseCleaner
-
1
module Generic
-
1
module Truncation
-
1
def initialize(opts={})
-
2
if !opts.empty? && !(opts.keys - [:only, :except, :pre_count, :reset_ids, :cache_tables]).empty?
-
raise ArgumentError, "The only valid options are :only, :except, :pre_count, :reset_ids or :cache_tables. You specified #{opts.keys.join(',')}."
-
end
-
2
if opts.has_key?(:only) && opts.has_key?(:except)
-
raise ArgumentError, "You may only specify either :only or :except. Doing both doesn't really make sense does it?"
-
end
-
-
2
@only = opts[:only]
-
2
@tables_to_exclude = Array( (opts[:except] || []).dup ).flatten
-
2
@tables_to_exclude += migration_storage_names
-
2
@pre_count = opts[:pre_count]
-
2
@reset_ids = opts[:reset_ids]
-
2
@cache_tables = opts.has_key?(:cache_tables) ? !!opts[:cache_tables] : true
-
end
-
-
1
def start
-
#included for compatability reasons, do nothing if you don't need to
-
end
-
-
1
def clean
-
raise NotImplementedError
-
end
-
-
1
private
-
1
def tables_to_truncate
-
raise NotImplementedError
-
end
-
-
# overwrite in subclasses
-
# default implementation given because migration storage need not be present
-
1
def migration_storage_names
-
%w[]
-
end
-
end
-
end
-
end
-
1
module DatabaseCleaner
-
1
class NullStrategy
-
1
def self.start
-
# no-op
-
end
-
-
1
def self.db=(connection)
-
# no-op
-
end
-
-
1
def self.clean
-
# no-op
-
end
-
end
-
end
-
1
require 'dm-core'
-
-
1
require 'dm-aggregates/aggregate_functions'
-
1
require 'dm-aggregates/collection'
-
1
require 'dm-aggregates/core_ext/symbol'
-
1
require 'dm-aggregates/model'
-
1
require 'dm-aggregates/query'
-
1
require 'dm-aggregates/repository'
-
-
1
module DataMapper
-
1
module Aggregates
-
1
def self.include_aggregate_api
-
1
[ :Repository, :Model, :Collection, :Query ].each do |name|
-
4
DataMapper.const_get(name).send(:include, const_get(name))
-
end
-
1
Adapters::AbstractAdapter.descendants.each do |adapter_class|
-
Adapters.include_aggregate_api(DataMapper::Inflector.demodulize(adapter_class.name))
-
end
-
end
-
end
-
-
1
module Adapters
-
-
1
def self.include_aggregate_api(const_name)
-
2
require aggregate_extensions(const_name)
-
1
if Aggregates.const_defined?(const_name)
-
1
adapter = const_get(const_name)
-
1
adapter.send(:include, aggregate_module(const_name))
-
end
-
rescue LoadError
-
# Silently ignore the fact that no adapter extensions could be required
-
# This means that the adapter in use doesn't support aggregates
-
end
-
-
1
def self.aggregate_module(const_name)
-
1
Aggregates.const_get(const_name)
-
end
-
-
1
class << self
-
1
private
-
# @api private
-
1
def aggregate_extensions(const_name)
-
2
name = adapter_name(const_name)
-
2
name = 'do' if name == 'dataobjects'
-
2
"dm-aggregates/adapters/dm-#{name}-adapter"
-
end
-
end
-
-
1
extendable do
-
# @api private
-
1
def const_added(const_name)
-
2
include_aggregate_api(const_name)
-
2
super
-
end
-
end
-
end # module Adapters
-
-
1
Aggregates.include_aggregate_api
-
-
end # module DataMapper
-
1
module DataMapper
-
1
module Aggregates
-
1
module DataObjectsAdapter
-
1
extend Chainable
-
-
1
def aggregate(query)
-
12
fields = query.fields
-
24
types = fields.map { |p| p.respond_to?(:operator) ? String : p.primitive }
-
-
12
field_size = fields.size
-
-
12
records = []
-
-
12
with_connection do |connection|
-
12
statement, bind_values = select_statement(query)
-
-
12
command = connection.create_command(statement)
-
12
command.set_types(types)
-
-
12
reader = command.execute_reader(*bind_values)
-
-
12
begin
-
36
while(reader.next!)
-
12
row = fields.zip(reader.values).map do |field, value|
-
12
if field.respond_to?(:operator)
-
12
send(field.operator, field.target, value)
-
else
-
field.load(value)
-
end
-
end
-
-
12
records << (field_size > 1 ? row : row[0])
-
end
-
ensure
-
12
reader.close
-
end
-
end
-
-
12
records
-
end
-
-
1
private
-
-
1
def count(property, value)
-
12
value.to_i
-
end
-
-
1
def min(property, value)
-
property.load(value)
-
end
-
-
1
def max(property, value)
-
property.load(value)
-
end
-
-
1
def avg(property, value)
-
property.primitive == ::Integer ? value.to_f : property.load(value)
-
end
-
-
1
def sum(property, value)
-
property.load(value)
-
end
-
-
1
chainable do
-
1
def property_to_column_name(property, qualify)
-
304
case property
-
when DataMapper::Query::Operator
-
12
aggregate_field_statement(property.operator, property.target, qualify)
-
-
when Property, DataMapper::Query::Path
-
292
super
-
-
else
-
raise ArgumentError, "+property+ must be a DataMapper::Query::Operator, a DataMapper::Property or a Query::Path, but was a #{property.class} (#{property.inspect})"
-
end
-
end
-
end
-
-
1
def aggregate_field_statement(aggregate_function, property, qualify)
-
12
column_name = if aggregate_function == :count && property == :all
-
12
'*'
-
else
-
property_to_column_name(property, qualify)
-
end
-
-
12
function_name = case aggregate_function
-
12
when :count then 'COUNT'
-
when :min then 'MIN'
-
when :max then 'MAX'
-
when :avg then 'AVG'
-
when :sum then 'SUM'
-
else raise "Invalid aggregate function: #{aggregate_function.inspect}"
-
end
-
-
12
"#{function_name}(#{column_name})"
-
end
-
-
end # class DataObjectsAdapter
-
end # module Aggregates
-
end # module DataMapper
-
1
require 'dm-aggregates/functions'
-
1
module DataMapper
-
1
module Aggregates
-
1
module Collection
-
1
include Functions
-
-
1
private
-
-
1
def property_by_name(property_name)
-
properties[property_name]
-
end
-
end
-
end
-
end
-
1
require 'dm-aggregates/operators'
-
-
1
class Symbol
-
-
1
include DataMapper::Aggregates::Operators
-
-
end
-
1
module DataMapper
-
1
module Aggregates
-
1
module Functions
-
1
include DataMapper::Assertions
-
-
# Count results (given the conditions)
-
#
-
# @example the count of all friends
-
# Friend.count
-
#
-
# @example the count of all friends older then 18
-
# Friend.count(:age.gt => 18)
-
#
-
# @example the count of all your female friends
-
# Friend.count(:conditions => [ 'gender = ?', 'female' ])
-
#
-
# @example the count of all friends with an address (NULL values are not included)
-
# Friend.count(:address)
-
#
-
# @example the count of all friends with an address that are older then 18
-
# Friend.count(:address, :age.gt => 18)
-
#
-
# @example the count of all your female friends with an address
-
# Friend.count(:address, :conditions => [ 'gender = ?', 'female' ])
-
#
-
# @param property [Symbol] of the property you with to count (optional)
-
# @param opts [Hash, Symbol] the conditions
-
#
-
# @return [Integer] return the count given the conditions
-
#
-
# @api public
-
1
def count(*args)
-
12
query = args.last.kind_of?(Hash) ? args.pop : {}
-
12
property_name = args.first
-
-
12
if property_name
-
assert_kind_of 'property', property_by_name(property_name), Property
-
end
-
-
12
aggregate(query.merge(:fields => [ property_name ? property_name.count : :all.count ])).to_i
-
end
-
-
# Get the lowest value of a property
-
#
-
# @example the age of the youngest friend
-
# Friend.min(:age)
-
#
-
# @example the age of the youngest female friend
-
# Friend.min(:age, :conditions => [ 'gender = ?', 'female' ])
-
#
-
# @param property [Symbol] the property you wish to get the lowest value of
-
# @param opts [Hash, Symbol] the conditions
-
#
-
# @return [Integer] return the lowest value of a property given the conditions
-
#
-
# @api public
-
1
def min(*args)
-
query = args.last.kind_of?(Hash) ? args.pop : {}
-
property_name = args.first
-
-
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal, ::DateTime, ::Date, ::Time
-
-
aggregate(query.merge(:fields => [ property_name.min ]))
-
end
-
-
# Get the highest value of a property
-
#
-
# @example the age of the oldest friend
-
# Friend.max(:age)
-
#
-
# @example the age of the oldest female friend
-
# Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
-
#
-
# @param property [Symbol] the property you wish to get the highest value of
-
# @param opts [Hash, Symbol] the conditions
-
#
-
# @return [Integer] return the highest value of a property given the conditions
-
#
-
# @api public
-
1
def max(*args)
-
query = args.last.kind_of?(Hash) ? args.pop : {}
-
property_name = args.first
-
-
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal, ::DateTime, ::Date, ::Time
-
-
aggregate(query.merge(:fields => [ property_name.max ]))
-
end
-
-
# Get the average value of a property
-
#
-
# @example the average age of all friends
-
# Friend.avg(:age)
-
#
-
# @example the average age of all female friends
-
# Friend.avg(:age, :conditions => [ 'gender = ?', 'female' ])
-
#
-
# @param property [Symbol] the property you wish to get the average value of
-
# @param opts [Hash, Symbol] the conditions
-
#
-
# @return [Integer] return the average value of a property given the conditions
-
#
-
# @api public
-
1
def avg(*args)
-
query = args.last.kind_of?(Hash) ? args.pop : {}
-
property_name = args.first
-
-
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
-
-
aggregate(query.merge(:fields => [ property_name.avg ]))
-
end
-
-
# Get the total value of a property
-
#
-
# @example the total age of all friends
-
# Friend.sum(:age)
-
#
-
# @example the total age of all female friends
-
# Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
-
#
-
# @param property [Symbol] the property you wish to get the total value of
-
# @param opts [Hash, Symbol] the conditions
-
#
-
# @return [Integer] return the total value of a property given the conditions
-
#
-
# @api public
-
1
def sum(*args)
-
query = args.last.kind_of?(::Hash) ? args.pop : {}
-
property_name = args.first
-
-
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
-
-
aggregate(query.merge(:fields => [ property_name.sum ]))
-
end
-
-
# Perform aggregate queries
-
#
-
# @example the count of friends
-
# Friend.aggregate(:all.count)
-
#
-
# @example the minimum age, the maximum age and the total age of friends
-
# Friend.aggregate(:age.min, :age.max, :age.sum)
-
#
-
# @example the average age, grouped by gender
-
# Friend.aggregate(:age.avg, :fields => [ :gender ])
-
#
-
# @param aggregates [Symbol, ...] operators to aggregate with
-
# @param query [Hash] the conditions
-
#
-
# @return [Array,Numeric,DateTime,Date,Time] the results of the
-
# aggregate query
-
#
-
# @api public
-
1
def aggregate(*args)
-
12
query = args.last.kind_of?(Hash) ? args.pop : {}
-
-
12
query[:fields] ||= []
-
12
query[:fields] |= args
-
24
query[:fields].map! { |f| normalize_field(f) }
-
-
12
raise ArgumentError, 'query[:fields] must not be empty' if query[:fields].empty?
-
-
12
unless query.key?(:order)
-
# the current collection/model is already sorted by attributes
-
# and since we are projecting away some of the attributes,
-
# and then performing aggregate functions on the remainder,
-
# we need to honor the existing order, as if it were already
-
# materialized, and we are looping over the rows in order.
-
-
12
directions = direction_map
-
-
12
query[:order] = []
-
-
# use the current query order for each property if available
-
12
query[:fields].each do |property|
-
12
next unless property.kind_of?(Property)
-
query[:order] << directions.fetch(property, property)
-
end
-
end
-
-
12
query = scoped_query(query)
-
-
24
if query.fields.any? { |p| p.kind_of?(Property) }
-
query.repository.aggregate(query.update(:unique => true))
-
else
-
12
query.repository.aggregate(query).first # only return one row
-
end
-
end
-
-
1
private
-
-
1
def assert_property_type(name, *types)
-
if name.nil?
-
raise ArgumentError, 'property name must not be nil'
-
end
-
-
property = property_by_name(name)
-
type = property.primitive
-
-
unless types.include?(type)
-
raise ArgumentError, "#{name} must be #{types * ' or '}, but was #{type}"
-
end
-
end
-
-
1
def normalize_field(field)
-
12
assert_kind_of 'field', field, DataMapper::Query::Operator, Symbol, Property
-
-
12
case field
-
when DataMapper::Query::Operator
-
12
if field.target == :all
-
12
field
-
else
-
field.class.new(property_by_name(field.target), field.operator)
-
end
-
-
when Symbol
-
property_by_name(field)
-
-
when Property
-
field
-
end
-
end
-
-
1
def direction_map
-
12
direction_map = {}
-
12
self.query.order.each do |direction|
-
12
direction_map[direction.target] = direction
-
end
-
12
direction_map
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
1
module Aggregates
-
1
module Model
-
1
include Functions
-
-
1
private
-
-
1
def property_by_name(property_name)
-
properties(repository.name)[property_name]
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
1
module Aggregates
-
1
module Operators
-
1
def count
-
12
DataMapper::Query::Operator.new(self, :count)
-
end
-
-
1
def min
-
DataMapper::Query::Operator.new(self, :min)
-
end
-
-
1
def max
-
DataMapper::Query::Operator.new(self, :max)
-
end
-
-
1
def avg
-
DataMapper::Query::Operator.new(self, :avg)
-
end
-
-
1
def sum
-
DataMapper::Query::Operator.new(self, :sum)
-
end
-
end # module Operators
-
end # module Aggregates
-
end # module DataMapper
-
1
module DataMapper
-
1
module Aggregates
-
1
module Query
-
1
def self.included(base)
-
1
base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
# FIXME: figure out a cleaner approach than AMC
-
alias_method :assert_valid_fields_without_operator, :assert_valid_fields
-
alias_method :assert_valid_fields, :assert_valid_fields_with_operator
-
RUBY
-
end
-
-
1
def assert_valid_fields_with_operator(fields, unique)
-
200
operators, fields = fields.partition { |f| f.kind_of?(DataMapper::Query::Operator) }
-
-
91
operators.each do |operator|
-
12
target = operator.target
-
-
12
unless target == :all || @properties.include?(target)
-
raise ArgumentError, "+options[:fields]+ entry #{target.inspect} does not map to a property in #{model}"
-
end
-
end
-
-
91
assert_valid_fields_without_operator(fields, unique)
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
1
module Aggregates
-
1
module Repository
-
1
def aggregate(query)
-
12
unless query.valid?
-
[]
-
else
-
12
adapter.aggregate(query)
-
end
-
end
-
end
-
end
-
end
-
1
require "data_mapper/constraints/adapters/extension"
-
-
1
module DataMapper
-
1
module Constraints
-
1
module Adapters
-
1
module AbstractAdapter
-
-
# @api private
-
1
def constraint_exists?(*)
-
false
-
end
-
-
# @api private
-
1
def create_relationship_constraint(*)
-
false
-
end
-
-
# @api private
-
1
def destroy_relationship_constraint(*)
-
false
-
end
-
-
end # module AbstractAdapter
-
end # module Adapters
-
end # module Constraints
-
-
1
Adapters::AbstractAdapter.class_eval do
-
1
include Constraints::Adapters::AbstractAdapter
-
end
-
-
1
Adapters::AbstractAdapter.descendants.each do |adapter_class|
-
const_name = DataMapper::Inflector.demodulize(adapter_class.name)
-
Adapters.include_constraint_api(const_name)
-
end
-
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Adapters
-
-
1
module DataObjectsAdapter
-
##
-
# Determine if a constraint exists for a table
-
#
-
# @param storage_name [Symbol]
-
# name of table to check constraint on
-
# @param constraint_name [~String]
-
# name of constraint to check for
-
#
-
# @return [Boolean]
-
#
-
# @api private
-
1
def constraint_exists?(storage_name, constraint_name)
-
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
-
SELECT COUNT(*)
-
FROM #{quote_name('information_schema')}.#{quote_name('table_constraints')}
-
WHERE #{quote_name('constraint_type')} = 'FOREIGN KEY'
-
AND #{quote_name('table_schema')} = ?
-
AND #{quote_name('table_name')} = ?
-
AND #{quote_name('constraint_name')} = ?
-
SQL
-
-
select(statement, schema_name, storage_name, constraint_name).first > 0
-
end
-
-
##
-
# Create the constraint for a relationship
-
#
-
# @param relationship [Relationship]
-
# the relationship to create the constraint for
-
#
-
# @return [true, false]
-
# true if creating the constraints was successful
-
#
-
# @api semipublic
-
1
def create_relationship_constraint(relationship)
-
return false unless valid_relationship_for_constraint?(relationship)
-
-
source_storage_name = relationship.source_model.storage_name(name)
-
target_storage_name = relationship.target_model.storage_name(name)
-
constraint_name = constraint_name(source_storage_name, relationship.name)
-
-
return false if constraint_exists?(source_storage_name, constraint_name)
-
-
constraint_type =
-
case relationship.inverse.constraint
-
when :protect then 'NO ACTION'
-
# TODO: support :cascade as an option:
-
# (destroy doesn't communicate the UPDATE constraint)
-
when :destroy, :destroy! then 'CASCADE'
-
when :set_nil then 'SET NULL'
-
end
-
-
return false if constraint_type.nil?
-
-
source_keys = relationship.source_key.map { |p| property_to_column_name(p, false) }
-
target_keys = relationship.target_key.map { |p| property_to_column_name(p, false) }
-
-
create_constraints_statement = create_constraints_statement(
-
constraint_name,
-
constraint_type,
-
source_storage_name,
-
source_keys,
-
target_storage_name,
-
target_keys)
-
-
execute(create_constraints_statement)
-
end
-
-
##
-
# Remove the constraint for a relationship
-
#
-
# @param relationship [Relationship]
-
# the relationship to remove the constraint for
-
#
-
# @return [true, false]
-
# true if destroying the constraint was successful
-
#
-
# @api semipublic
-
1
def destroy_relationship_constraint(relationship)
-
return false unless valid_relationship_for_constraint?(relationship)
-
-
storage_name = relationship.source_model.storage_name(name)
-
constraint_name = constraint_name(storage_name, relationship.name)
-
-
return false unless constraint_exists?(storage_name, constraint_name)
-
-
destroy_constraints_statement =
-
destroy_constraints_statement(storage_name, constraint_name)
-
-
execute(destroy_constraints_statement)
-
end
-
-
1
private
-
-
##
-
# Check to see if the relationship's constraints can be used
-
#
-
# Only one-to-one, one-to-many, and many-to-many relationships
-
# can be used for constraints. They must also be in the same
-
# repository as the adapter is connected to.
-
#
-
# @param relationship [Relationship]
-
# the relationship to check
-
#
-
# @return [true, false]
-
# true if a constraint can be established for relationship
-
#
-
# @api private
-
1
def valid_relationship_for_constraint?(relationship)
-
return false unless relationship.source_repository_name == name || relationship.source_repository_name.nil?
-
return false unless relationship.target_repository_name == name || relationship.target_repository_name.nil?
-
return false unless relationship.kind_of?(Associations::ManyToOne::Relationship)
-
true
-
end
-
-
1
module SQL
-
-
1
private
-
-
# Generates the SQL statement to create a constraint
-
#
-
# @param [String] constraint_name
-
# name of the foreign key constraint
-
# @param [String] constraint_type
-
# type of constraint to ALTER source_storage_name with
-
# @param [String] source_storage_name
-
# name of table to ALTER with constraint
-
# @param [Array(String)] source_keys
-
# columns in source_storage_name that refer to foreign table
-
# @param [String] target_storage_name
-
# target table of the constraint
-
# @param [Array(String)] target_keys
-
# columns the target table that are referred to
-
#
-
# @return [String]
-
# SQL DDL Statement to create a constraint
-
#
-
# @api private
-
1
def create_constraints_statement(constraint_name, constraint_type, source_storage_name, source_keys, target_storage_name, target_keys)
-
DataMapper::Ext::String.compress_lines(<<-SQL)
-
ALTER TABLE #{quote_name(source_storage_name)}
-
ADD CONSTRAINT #{quote_name(constraint_name)}
-
FOREIGN KEY (#{source_keys.join(', ')})
-
REFERENCES #{quote_name(target_storage_name)} (#{target_keys.join(', ')})
-
ON DELETE #{constraint_type}
-
ON UPDATE #{constraint_type}
-
SQL
-
end
-
-
##
-
# Generates the SQL statement to destroy a constraint
-
#
-
# @param [String] storage_name
-
# name of table to constrain
-
# @param [String] constraint_name
-
# name of foreign key constraint
-
#
-
# @return [String]
-
# SQL DDL Statement to destroy a constraint
-
#
-
# @api private
-
1
def destroy_constraints_statement(storage_name, constraint_name)
-
DataMapper::Ext::String.compress_lines(<<-SQL)
-
ALTER TABLE #{quote_name(storage_name)}
-
DROP CONSTRAINT #{quote_name(constraint_name)}
-
SQL
-
end
-
-
##
-
# generates a unique constraint name given a table and a relationships
-
#
-
# @param [String] storage_name
-
# name of table to constrain
-
# @param [String] relationship_name
-
# name of the relationship to constrain
-
#
-
# @return [String]
-
# name of the constraint
-
#
-
# @api private
-
1
def constraint_name(storage_name, relationship_name)
-
identifier = "#{storage_name}_#{relationship_name}"[0, self.class::IDENTIFIER_MAX_LENGTH - 3]
-
"#{identifier}_fk"
-
end
-
end
-
-
1
include SQL
-
-
end # module DataObjectsAdapter
-
-
end # module Adapters
-
end # module Constraints
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Adapters
-
1
module Extension
-
# Include the corresponding Constraints module into a adapter class
-
#
-
# @param [Symbol] const_name
-
# demodulized name of the adapter class to include corresponding
-
# constraints module into
-
#
-
# TODO: come up with a better way to include modules
-
# into all currently loaded and subsequently loaded Adapters
-
#
-
# @api private
-
1
def include_constraint_api(const_name)
-
2
require constraint_extensions(const_name)
-
-
2
if Constraints::Adapters.const_defined?(const_name)
-
2
adapter = const_get(const_name)
-
2
constraint_module = Constraints::Adapters.const_get(const_name)
-
4
adapter.class_eval { include constraint_module }
-
end
-
rescue LoadError
-
# Silently ignore the fact that no adapter extensions could be required
-
# This means that the adapter in use doesn't support constraints
-
end
-
-
1
private
-
-
# @api private
-
1
def constraint_extensions(const_name)
-
2
name = adapter_name(const_name)
-
2
name = 'do' if name == 'dataobjects'
-
2
"data_mapper/constraints/adapters/#{name}_adapter"
-
end
-
-
# @api private
-
1
def const_added(const_name)
-
2
include_constraint_api(const_name)
-
2
super
-
end
-
-
end # module Extension
-
end # module Adapters
-
end # module Constraints
-
-
1
Adapters.extend Constraints::Adapters::Extension
-
-
end # module DataMapper
-
1
require 'data_mapper/constraints/adapters/do_adapter'
-
-
1
module DataMapper
-
1
module Constraints
-
1
module Adapters
-
-
1
module PostgresAdapter
-
1
include DataObjectsAdapter
-
end
-
-
end
-
end
-
end
-
# TODO: figure out some other (less tightly coupled) way to ensure that
-
# dm-migrations' method implementations are loaded before this file
-
1
require "dm-migrations/auto_migration"
-
-
1
module DataMapper
-
1
module Constraints
-
1
module Migrations
-
1
module Model
-
-
# @api private
-
1
def auto_migrate_constraints_up(repository_name = self.repository_name)
-
# TODO: this check should not be here
-
return if self.respond_to?(:is_remixable?) && self.is_remixable?
-
-
relationships(repository_name).each do |relationship|
-
relationship.auto_migrate_constraints_up(repository_name)
-
end
-
end
-
-
# @api private
-
1
def auto_migrate_constraints_down(repository_name = self.repository_name)
-
return unless storage_exists?(repository_name)
-
# TODO: this check should not be here
-
return if self.respond_to?(:is_remixable?) && self.is_remixable?
-
-
relationships(repository_name).each do |relationship|
-
relationship.auto_migrate_constraints_down(repository_name)
-
end
-
end
-
-
end # module Model
-
end # module Migrations
-
end # module Constraints
-
-
1
Model.append_extensions Constraints::Migrations::Model
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Migrations
-
1
module Relationship
-
# @api private
-
1
def auto_migrate_constraints_up(repository_name)
-
# no-op
-
end
-
-
# @api private
-
1
def auto_migrate_constraints_down(repository_name)
-
# no-op
-
end
-
-
1
module ManyToOne
-
# @api private
-
1
def auto_migrate_constraints_up(repository_name)
-
adapter = DataMapper.repository(repository_name).adapter
-
adapter.create_relationship_constraint(self)
-
self
-
end
-
-
# @api private
-
1
def auto_migrate_constraints_down(repository_name)
-
adapter = DataMapper.repository(repository_name).adapter
-
adapter.destroy_relationship_constraint(self)
-
self
-
end
-
end
-
-
end # module Relationship
-
end # module Migrations
-
end # module Constraints
-
-
1
Associations::Relationship.class_eval do
-
1
include Constraints::Migrations::Relationship
-
end
-
-
1
Associations::ManyToOne::Relationship.class_eval do
-
1
include Constraints::Migrations::Relationship::ManyToOne
-
end
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Migrations
-
1
module SingletonMethods
-
-
1
def auto_migrate!(repository_name = nil)
-
auto_migrate_constraints_down(repository_name)
-
# TODO: Model#auto_migrate! drops and adds constraints, as well.
-
# is that an avoidable duplication?
-
super
-
auto_migrate_constraints_up(repository_name)
-
self
-
end
-
-
1
private
-
-
1
def auto_migrate_down!(repository_name = nil)
-
auto_migrate_constraints_down(repository_name)
-
super
-
self
-
end
-
-
1
def auto_migrate_up!(repository_name = nil)
-
super
-
auto_migrate_constraints_up(repository_name)
-
self
-
end
-
-
# @api private
-
1
def auto_migrate_constraints_up(repository_name = nil)
-
DataMapper::Model.descendants.each do |model|
-
model.auto_migrate_constraints_up(repository_name || model.default_repository_name)
-
end
-
end
-
-
# @api private
-
1
def auto_migrate_constraints_down(repository_name = nil)
-
DataMapper::Model.descendants.each do |model|
-
model.auto_migrate_constraints_down(repository_name || model.default_repository_name)
-
end
-
end
-
-
end # module SingletonMethods
-
end # module Migrations
-
end # module Constraints
-
-
1
extend Constraints::Migrations::SingletonMethods
-
end # module DataMapper
-
1
require 'data_mapper/constraints/relationship/one_to_many'
-
-
1
module DataMapper
-
1
module Constraints
-
1
module Relationship
-
1
module ManyToMany
-
-
1
private
-
-
1
def one_to_many_options
-
2
super.merge(:constraint => @constraint)
-
end
-
-
# Checks that the constraint type is appropriate to the relationship
-
#
-
# @param [Fixnum] cardinality
-
# cardinality of relationship
-
# @param [Symbol] name
-
# name of relationship to evaluate constraint of
-
# @param [Hash] options
-
# options hash
-
#
-
# @option *args :constraint[Symbol]
-
# one of VALID_CONSTRAINT_VALUES
-
#
-
# @raise ArgumentError
-
# if @option :constraint is not one of VALID_CONSTRAINT_TYPES
-
#
-
# @return [Undefined]
-
#
-
# @api private
-
1
def assert_valid_constraint
-
2
super
-
-
# TODO: is any constraint valid for a m:m relationship?
-
2
if @constraint == :set_nil
-
raise ArgumentError, "#{@constraint} is not a valid constraint type for #{self.class}"
-
end
-
end
-
-
end # module ManyToMany
-
end # module Relationship
-
end # module Constraints
-
-
1
Associations::ManyToMany::Relationship::OPTIONS << :constraint
-
-
1
Associations::ManyToMany::Relationship.class_eval do
-
1
include Constraints::Relationship::ManyToMany
-
end
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Relationship
-
1
module OneToMany
-
-
1
attr_reader :constraint
-
-
# @api private
-
1
def enforce_destroy_constraint(resource)
-
return true unless association = get(resource)
-
-
constraint = self.constraint
-
-
case constraint
-
when :protect
-
Array(association).empty?
-
when :destroy, :destroy!
-
association.__send__(constraint)
-
when :set_nil
-
Array(association).all? do |resource|
-
resource.update(inverse => nil)
-
end
-
when :skip
-
true # do nothing
-
end
-
end
-
-
1
private
-
-
##
-
# Adds the delete constraint options to a relationship
-
#
-
# @param params [*ARGS] Arguments passed to Relationship#initialize
-
#
-
# @return [nil]
-
#
-
# @api private
-
1
def initialize(*args)
-
6
super
-
6
set_constraint
-
6
assert_valid_constraint
-
end
-
-
1
def set_constraint
-
6
@constraint = @options.fetch(:constraint, :protect) || :skip
-
end
-
-
# Checks that the constraint type is appropriate to the relationship
-
#
-
# @param [Fixnum] cardinality
-
# cardinality of relationship
-
# @param [Symbol] name
-
# name of relationship to evaluate constraint of
-
# @param [Hash] options
-
# options hash
-
#
-
# @option *args :constraint[Symbol]
-
# one of VALID_CONSTRAINT_VALUES
-
#
-
# @raise ArgumentError
-
# if @option :constraint is not one of VALID_CONSTRAINT_VALUES
-
#
-
# @return [Undefined]
-
#
-
# @api semipublic
-
1
def assert_valid_constraint
-
6
return unless @constraint
-
-
6
unless VALID_CONSTRAINT_VALUES.include?(@constraint)
-
raise ArgumentError, ":constraint option must be one of #{VALID_CONSTRAINT_VALUES.to_a.join(', ')}"
-
end
-
end
-
-
end # module OneToMany
-
end # module Relationship
-
end # module Constraints
-
-
1
Associations::OneToMany::Relationship::OPTIONS << :constraint
-
-
1
Associations::OneToMany::Relationship.class_eval do
-
1
include Constraints::Relationship::OneToMany
-
end
-
end # module DataMapper
-
1
module DataMapper
-
1
module Constraints
-
1
module Resource
-
1
def before_destroy_hook
-
enforce_destroy_constraints
-
super
-
end
-
-
1
private
-
-
# Check delete constraints prior to destroying a dm resource or collection
-
#
-
# @note
-
# - It only considers a relationship's constraints if this is the parent model (ie a child shouldn't delete a parent)
-
# - Many to Many Relationships are skipped, as they are evaluated by their underlying 1:M relationships
-
#
-
# @return [nil]
-
#
-
# @api semi-public
-
1
def enforce_destroy_constraints
-
relationships.each do |relationship|
-
next unless relationship.respond_to?(:enforce_destroy_constraint)
-
-
constraint_satisfied = relationship.enforce_destroy_constraint(self)
-
-
throw(:halt, false) unless constraint_satisfied
-
end
-
end
-
-
end # module Resource
-
end # module Constraints
-
-
1
Model.append_inclusions Constraints::Resource
-
end # module DataMapper
-
1
require 'dm-core'
-
-
1
require 'data_mapper/constraints/resource'
-
-
1
require 'data_mapper/constraints/migrations/model'
-
1
require 'data_mapper/constraints/migrations/relationship'
-
1
require 'data_mapper/constraints/migrations/singleton_methods'
-
-
1
require 'data_mapper/constraints/relationship/one_to_many'
-
1
require 'data_mapper/constraints/relationship/many_to_many'
-
-
1
require 'data_mapper/constraints/adapters/extension'
-
1
require 'data_mapper/constraints/adapters/abstract_adapter'
-
-
1
module DataMapper
-
1
module Constraints
-
1
VALID_CONSTRAINT_VALUES = [ :protect, :destroy, :destroy!, :set_nil, :skip ].to_set.freeze
-
end
-
end
-
1
require 'addressable/uri'
-
1
require 'bigdecimal'
-
1
require 'bigdecimal/util'
-
1
require 'date'
-
1
require 'pathname'
-
1
require 'set'
-
1
require 'time'
-
1
require 'yaml'
-
-
1
module DataMapper
-
1
module Undefined; end
-
end
-
-
1
require 'dm-core/support/ext/blank'
-
1
require 'dm-core/support/ext/hash'
-
1
require 'dm-core/support/ext/object'
-
1
require 'dm-core/support/ext/string'
-
-
1
begin
-
1
require 'fastthread'
-
rescue LoadError
-
# fastthread not installed
-
end
-
-
1
require 'dm-core/core_ext/pathname'
-
1
require 'dm-core/support/ext/module'
-
1
require 'dm-core/support/ext/array'
-
1
require 'dm-core/support/ext/try_dup'
-
-
1
require 'dm-core/support/mash'
-
1
require 'dm-core/support/inflector/inflections'
-
1
require 'dm-core/support/inflector/methods'
-
1
require 'dm-core/support/inflections'
-
1
require 'dm-core/support/chainable'
-
1
require 'dm-core/support/deprecate'
-
1
require 'dm-core/support/descendant_set'
-
1
require 'dm-core/support/equalizer'
-
1
require 'dm-core/support/assertions'
-
1
require 'dm-core/support/lazy_array'
-
1
require 'dm-core/support/local_object_space'
-
1
require 'dm-core/support/hook'
-
1
require 'dm-core/support/subject'
-
1
require 'dm-core/support/ordered_set'
-
1
require 'dm-core/support/subject_set'
-
-
1
require 'dm-core/query'
-
1
require 'dm-core/query/conditions/operation'
-
1
require 'dm-core/query/conditions/comparison'
-
1
require 'dm-core/query/operator'
-
1
require 'dm-core/query/direction'
-
1
require 'dm-core/query/path'
-
1
require 'dm-core/query/sort'
-
-
1
require 'dm-core/resource'
-
1
require 'dm-core/resource/persistence_state'
-
1
require 'dm-core/resource/persistence_state/transient'
-
1
require 'dm-core/resource/persistence_state/immutable'
-
1
require 'dm-core/resource/persistence_state/persisted'
-
1
require 'dm-core/resource/persistence_state/clean'
-
1
require 'dm-core/resource/persistence_state/deleted'
-
1
require 'dm-core/resource/persistence_state/dirty'
-
-
1
require 'dm-core/property'
-
1
require 'dm-core/property/typecast/numeric'
-
1
require 'dm-core/property/typecast/time'
-
1
require 'dm-core/property/object'
-
1
require 'dm-core/property/string'
-
1
require 'dm-core/property/binary'
-
1
require 'dm-core/property/text'
-
1
require 'dm-core/property/numeric'
-
1
require 'dm-core/property/float'
-
1
require 'dm-core/property/decimal'
-
1
require 'dm-core/property/boolean'
-
1
require 'dm-core/property/integer'
-
1
require 'dm-core/property/serial'
-
1
require 'dm-core/property/date'
-
1
require 'dm-core/property/date_time'
-
1
require 'dm-core/property/time'
-
1
require 'dm-core/property/class'
-
1
require 'dm-core/property/discriminator'
-
1
require 'dm-core/property/lookup'
-
1
require 'dm-core/property_set'
-
-
1
require 'dm-core/model'
-
1
require 'dm-core/model/hook'
-
1
require 'dm-core/model/is'
-
1
require 'dm-core/model/scope'
-
1
require 'dm-core/model/relationship'
-
1
require 'dm-core/model/property'
-
-
1
require 'dm-core/collection'
-
1
require 'dm-core/relationship_set'
-
1
require 'dm-core/associations/relationship'
-
1
require 'dm-core/associations/one_to_many'
-
1
require 'dm-core/associations/one_to_one'
-
1
require 'dm-core/associations/many_to_one'
-
1
require 'dm-core/associations/many_to_many'
-
-
1
require 'dm-core/identity_map'
-
1
require 'dm-core/repository'
-
1
require 'dm-core/adapters'
-
1
require 'dm-core/adapters/abstract_adapter'
-
-
1
require 'dm-core/support/logger'
-
1
require 'dm-core/support/naming_conventions'
-
1
require 'dm-core/version'
-
-
1
require 'dm-core/core_ext/kernel' # TODO: do not load automatically
-
1
require 'dm-core/core_ext/symbol' # TODO: do not load automatically
-
-
1
require 'dm-core/backwards' # TODO: do not load automatically
-
-
# A logger should always be present. Lets be consistent with DO
-
1
DataMapper::Logger.new(StringIO.new, :fatal)
-
-
1
unless defined?(Infinity)
-
1
Infinity = 1.0/0
-
end
-
-
# == Setup and Configuration
-
# DataMapper uses URIs or a connection hash to connect to your data-store.
-
# URI connections takes the form of:
-
# DataMapper.setup(:default, 'protocol://username:password@localhost:port/path/to/repo')
-
#
-
# Breaking this down, the first argument is the name you wish to give this
-
# connection. If you do not specify one, it will be assigned :default. If you
-
# would like to connect to more than one data-store, simply issue this command
-
# again, but with a different name specified.
-
#
-
# In order to issue ORM commands without specifying the repository context, you
-
# must define the :default database. Otherwise, you'll need to wrap your ORM
-
# calls in <tt>repository(:name) { }</tt>.
-
#
-
# Second, the URI breaks down into the access protocol, the username, the
-
# server, the password, and whatever path information is needed to properly
-
# address the data-store on the server.
-
#
-
# Here's some examples
-
# DataMapper.setup(:default, 'sqlite3://path/to/your/project/db/development.db')
-
# DataMapper.setup(:default, 'mysql://localhost/dm_core_test')
-
# # no auth-info
-
# DataMapper.setup(:default, 'postgres://root:supahsekret@127.0.0.1/dm_core_test')
-
# # with auth-info
-
#
-
#
-
# Alternatively, you can supply a hash as the second parameter, which would
-
# take the form:
-
#
-
# DataMapper.setup(:default, {
-
# :adapter => 'adapter_name_here',
-
# :database => 'path/to/repo',
-
# :username => 'username',
-
# :password => 'password',
-
# :host => 'hostname'
-
# })
-
#
-
# === Logging
-
# To turn on error logging to STDOUT, issue:
-
#
-
# DataMapper::Logger.new($stdout, :debug)
-
#
-
# You can pass a file location ("/path/to/log/file.log") in place of $stdout.
-
# see DataMapper::Logger for more information.
-
#
-
1
module DataMapper
-
1
extend DataMapper::Assertions
-
-
1
class RepositoryNotSetupError < StandardError; end
-
-
1
class IncompleteModelError < StandardError; end
-
-
1
class PluginNotFoundError < StandardError; end
-
-
1
class UnknownRelationshipError < StandardError; end
-
-
1
class ObjectNotFoundError < RuntimeError; end
-
-
1
class PersistenceError < RuntimeError; end
-
-
1
class UpdateConflictError < PersistenceError; end
-
-
1
class SaveFailureError < PersistenceError
-
1
attr_reader :resource
-
-
1
def initialize(message, resource)
-
super(message)
-
@resource = resource
-
end
-
end
-
-
1
class ImmutableError < RuntimeError; end
-
-
1
class ImmutableDeletedError < ImmutableError; end
-
-
# Raised on attempt to operate on collection of child objects
-
# when parent object is not yet saved.
-
# For instance, if your article object is not saved,
-
# but you try to fetch or scope down comments (1:n case), or
-
# publications (n:m case), operation cannot be completed
-
# because parent object's keys are not yet persisted,
-
# and thus there is no FK value to use in the query.
-
1
class UnsavedParentError < PersistenceError; end
-
-
# @api private
-
1
def self.root
-
@root ||= Pathname(__FILE__).dirname.parent.expand_path.freeze
-
end
-
-
# Setups up a connection to a data-store
-
#
-
# @param [Symbol] name
-
# a name for the context, defaults to :default
-
# @param [Hash(Symbol => String), Addressable::URI, String] uri_or_options
-
# connection information
-
#
-
# @return [DataMapper::Adapters::AbstractAdapter]
-
# the resulting setup adapter
-
#
-
# @raise [ArgumentError] "+name+ must be a Symbol, but was..."
-
# indicates that an invalid argument was passed for name[Symbol]
-
# @raise [ArgumentError] "+uri_or_options+ must be a Hash, URI or String, but was..."
-
# indicates that connection information could not be gleaned from
-
# the given uri_or_options[Hash, Addressable::URI, String]
-
#
-
# @api public
-
1
def self.setup(*args)
-
1
adapter = args.first
-
-
1
unless adapter.kind_of?(Adapters::AbstractAdapter)
-
1
adapter = Adapters.new(*args)
-
end
-
-
1
Repository.adapters[adapter.name] = adapter
-
end
-
-
# Block Syntax
-
# Pushes the named repository onto the context-stack,
-
# yields a new session, and pops the context-stack.
-
#
-
# Non-Block Syntax
-
# Returns the current session, or if there is none,
-
# a new Session.
-
#
-
# @param [Symbol] args the name of a repository to act within or return, :default is default
-
#
-
# @yield [Proc] (optional) block to execute within the context of the named repository
-
#
-
# @api public
-
1
def self.repository(name = nil)
-
1174
context = Repository.context
-
-
1174
current_repository = if name
-
1174
name = name.to_sym
-
1348
context.detect { |repository| repository.name == name }
-
else
-
name = Repository.default_name
-
context.last
-
end
-
-
1174
current_repository ||= Repository.new(name)
-
-
1174
if block_given?
-
158
current_repository.scope { |*block_args| yield(*block_args) }
-
else
-
1095
current_repository
-
end
-
end
-
-
# Perform necessary steps to finalize DataMapper for the current repository
-
#
-
# This method should be called after loading all models and plugins.
-
#
-
# It ensures foreign key properties and anonymous join models are created.
-
# These are otherwise lazily declared, which can lead to unexpected errors.
-
# It also performs basic validity checking of the DataMapper models.
-
#
-
# @return [DataMapper] The DataMapper module
-
#
-
# @api public
-
1
def self.finalize
-
6
Model.descendants.each { |model| model.finalize }
-
1
self
-
end
-
-
end # module DataMapper
-
1
module DataMapper
-
1
module Adapters
-
1
extend Chainable
-
1
extend DataMapper::Assertions
-
-
# Set up an adapter for a storage engine
-
#
-
# @see DataMapper.setup
-
#
-
# @api private
-
1
def self.new(repository_name, options)
-
1
options = normalize_options(options)
-
1
adapter_class(options.fetch(:adapter)).new(repository_name, options)
-
end
-
-
# The path used to require the in memory adapter
-
#
-
# Set this if you want to register your own adapter
-
# to be used when you specify an 'in_memory' connection
-
# during
-
#
-
# @see DataMapper.setup
-
#
-
# @param [String] path
-
# the path used to require the desired in memory adapter
-
#
-
# @api semipublic
-
1
def self.in_memory_adapter_path=(path)
-
@in_memory_adapter_path = path
-
end
-
-
# The path used to require the in memory adapter
-
#
-
# @see DataMapper.setup
-
#
-
# @return [String]
-
# the path used to require the desired in memory adapter
-
#
-
# @api semipublic
-
1
def self.in_memory_adapter_path
-
@in_memory_adapter_path ||= 'dm-core/adapters/in_memory_adapter'
-
end
-
-
1
class << self
-
1
private
-
-
# Normalize the arguments passed to new()
-
#
-
# Turns options hash or connection URI into the options hash used
-
# by the adapter.
-
#
-
# @param [Hash, Addressable::URI, String] options
-
# the options to be normalized
-
#
-
# @return [Mash]
-
# the options normalized as a Mash
-
#
-
# @api private
-
1
def normalize_options(options)
-
1
case options
-
when Hash then normalize_options_hash(options)
-
when Addressable::URI then normalize_options_uri(options)
-
1
when String then normalize_options_string(options)
-
else
-
assert_kind_of 'options', options, Hash, Addressable::URI, String
-
end
-
end
-
-
# Normalize Hash options into a Mash
-
#
-
# @param [Hash] hash
-
# the hash to be normalized
-
#
-
# @return [Mash]
-
# the options normalized as a Mash
-
#
-
# @api private
-
1
def normalize_options_hash(hash)
-
1
DataMapper::Ext::Hash.to_mash(hash)
-
end
-
-
# Normalize Addressable::URI options into a Mash
-
#
-
# @param [Addressable::URI] uri
-
# the uri to be normalized
-
#
-
# @return [Mash]
-
# the options normalized as a Mash
-
#
-
# @api private
-
1
def normalize_options_uri(uri)
-
1
options = normalize_options_hash(uri.to_hash)
-
-
# Extract the name/value pairs from the query portion of the
-
# connection uri, and set them as options directly.
-
1
if options.fetch(:query)
-
options.update(uri.query_values)
-
end
-
-
1
options[:adapter] = options.fetch(:scheme)
-
-
1
options
-
end
-
-
# Normalize String options into a Mash
-
#
-
# @param [String] string
-
# the string to be normalized
-
#
-
# @return [Mash]
-
# the options normalized as a Mash
-
#
-
# @api private
-
1
def normalize_options_string(string)
-
1
normalize_options_uri(Addressable::URI.parse(string))
-
end
-
-
# Return the adapter class constant
-
#
-
# @example
-
# DataMapper::Adapters.send(:adapter_class, 'mysql') # => DataMapper::Adapters::MysqlAdapter
-
#
-
# @param [Symbol] name
-
# the name of the adapter
-
#
-
# @return [Class]
-
# the AbstractAdapter subclass
-
#
-
# @api private
-
1
def adapter_class(name)
-
1
adapter_name = normalize_adapter_name(name)
-
1
class_name = (DataMapper::Inflector.camelize(adapter_name) << 'Adapter').to_sym
-
1
load_adapter(adapter_name) unless const_defined?(class_name)
-
1
const_get(class_name)
-
end
-
-
# Return the name of the adapter
-
#
-
# @example
-
# DataMapper::Adapters.adapter_name('MysqlAdapter') # => 'mysql'
-
#
-
# @param [String] const_name
-
# the adapter constant name
-
#
-
# @return [String]
-
# the name of the adapter
-
#
-
# @api semipublic
-
1
def adapter_name(const_name)
-
8
const_name.to_s.chomp('Adapter').downcase
-
end
-
-
# Require the adapter library
-
#
-
# @param [String, Symbol] name
-
# the name of the adapter
-
#
-
# @return [Boolean]
-
# true if the adapter is loaded
-
#
-
# @api private
-
1
def load_adapter(name)
-
require "dm-#{name}-adapter"
-
rescue LoadError => original_error
-
begin
-
require in_memory_adapter?(name) ? in_memory_adapter_path : legacy_path(name)
-
rescue LoadError
-
raise original_error
-
end
-
end
-
-
# Returns wether or not the given adapter name is considered an in memory adapter
-
#
-
# @param [String, Symbol] name
-
# the name of the adapter
-
#
-
# @return [Boolean]
-
# true if the adapter is considered to be an in memory adapter
-
#
-
# @api private
-
1
def in_memory_adapter?(name)
-
name.to_s == 'in_memory'
-
end
-
-
# Returns the fallback filename that would be used to require the named adapter
-
#
-
# The fallback format is "#{name}_adapter" and will be phased out in favor of
-
# the properly 'namespaced' "dm-#{name}-adapter" format.
-
#
-
# @param [String, Symbol] name
-
# the name of the adapter to require
-
#
-
# @return [String]
-
# the filename that gets required for the adapter identified by name
-
#
-
# @api private
-
1
def legacy_path(name)
-
"#{name}_adapter"
-
end
-
-
# Adjust the adapter name to match the name used in the gem providing the adapter
-
#
-
# @param [String, Symbol] name
-
# the name of the adapter
-
#
-
# @return [String]
-
# the normalized adapter name
-
#
-
# @api private
-
1
def normalize_adapter_name(name)
-
1
(original = name.to_s) == 'sqlite3' ? 'sqlite' : original
-
end
-
-
end
-
-
1
extendable do
-
# @api private
-
1
def const_added(const_name)
-
end
-
end
-
end # module Adapters
-
end # module DataMapper
-
1
module DataMapper
-
1
module Adapters
-
# Specific adapters extend this class and implement
-
# methods for creating, reading, updating and deleting records.
-
#
-
# Adapters may only implement method for reading or (less common case)
-
# writing. Read only adapter may be useful when one needs to work
-
# with legacy data that should not be changed or web services that
-
# only provide read access to data (from Wordnet and Medline to
-
# Atom and RSS syndication feeds)
-
#
-
# Note that in case of adapters to relational databases it makes
-
# sense to inherit from DataObjectsAdapter class.
-
1
class AbstractAdapter
-
1
include DataMapper::Assertions
-
1
extend DataMapper::Assertions
-
1
extend Equalizer
-
-
1
equalize :name, :options, :resource_naming_convention, :field_naming_convention
-
-
# @api semipublic
-
1
def self.descendants
-
7
@descendants ||= DescendantSet.new
-
end
-
-
# @api private
-
1
def self.inherited(descendant)
-
3
descendants << descendant
-
end
-
-
# Adapter name
-
#
-
# @example
-
# adapter.name # => :default
-
#
-
# Note that when you use
-
#
-
# DataMapper.setup(:default, 'postgres://postgres@localhost/dm_core_test')
-
#
-
# the adapter name is currently set to :default
-
#
-
# @return [Symbol]
-
# the adapter name
-
#
-
# @api semipublic
-
1
attr_reader :name
-
-
# Options with which adapter was set up
-
#
-
# @example
-
# adapter.options # => { :adapter => 'yaml', :path => '/tmp' }
-
#
-
# @return [Hash]
-
# adapter configuration options
-
#
-
# @api semipublic
-
1
attr_reader :options
-
-
# A callable object returning a naming convention for model storage
-
#
-
# @example
-
# adapter.resource_naming_convention # => Proc for model storage name
-
#
-
# @return [#call]
-
# object to return the naming convention for each model
-
#
-
# @api semipublic
-
1
attr_accessor :resource_naming_convention
-
-
# A callable object returning a naming convention for property fields
-
#
-
# @example
-
# adapter.field_naming_convention # => Proc for field name
-
#
-
# @return [#call]
-
# object to return the naming convention for each field
-
#
-
# @api semipublic
-
1
attr_accessor :field_naming_convention
-
-
# Persists one or many new resources
-
#
-
# @example
-
# adapter.create(collection) # => 1
-
#
-
# Adapters provide specific implementation of this method
-
#
-
# @param [Enumerable<Resource>] resources
-
# The list of resources (model instances) to create
-
#
-
# @return [Integer]
-
# The number of records that were actually saved into the data-store
-
#
-
# @api semipublic
-
1
def create(resources)
-
raise NotImplementedError, "#{self.class}#create not implemented"
-
end
-
-
# Reads one or many resources from a datastore
-
#
-
# @example
-
# adapter.read(query) # => [ { 'name' => 'Dan Kubb' } ]
-
#
-
# Adapters provide specific implementation of this method
-
#
-
# @param [Query] query
-
# the query to match resources in the datastore
-
#
-
# @return [Enumerable<Hash>]
-
# an array of hashes to become resources
-
#
-
# @api semipublic
-
1
def read(query)
-
raise NotImplementedError, "#{self.class}#read not implemented"
-
end
-
-
# Updates one or many existing resources
-
#
-
# @example
-
# adapter.update(attributes, collection) # => 1
-
#
-
# Adapters provide specific implementation of this method
-
#
-
# @param [Hash(Property => Object)] attributes
-
# hash of attribute values to set, keyed by Property
-
# @param [Collection] collection
-
# collection of records to be updated
-
#
-
# @return [Integer]
-
# the number of records updated
-
#
-
# @api semipublic
-
1
def update(attributes, collection)
-
raise NotImplementedError, "#{self.class}#update not implemented"
-
end
-
-
# Deletes one or many existing resources
-
#
-
# @example
-
# adapter.delete(collection) # => 1
-
#
-
# Adapters provide specific implementation of this method
-
#
-
# @param [Collection] collection
-
# collection of records to be deleted
-
#
-
# @return [Integer]
-
# the number of records deleted
-
#
-
# @api semipublic
-
1
def delete(collection)
-
raise NotImplementedError, "#{self.class}#delete not implemented"
-
end
-
-
# Create a Query object or subclass.
-
#
-
# Alter this method if you'd like to return an adapter specific Query subclass.
-
#
-
# @param [Repository] repository
-
# the Repository to retrieve results from
-
# @param [Model] model
-
# the Model to retrieve results from
-
# @param [Hash] options
-
# the conditions and scope
-
#
-
# @return [Query]
-
#
-
# @api semipublic
-
#--
-
# TODO: DataObjects::Connection.create_command style magic (Adapter)::Query?
-
1
def new_query(repository, model, options = {})
-
146
Query.new(repository, model, options)
-
end
-
-
1
protected
-
-
# Set the serial value of the Resource
-
#
-
# @param [Resource] resource
-
# the resource to set the serial property in
-
# @param [Integer] next_id
-
# the identifier to set in the resource
-
#
-
# @return [undefined]
-
#
-
# @api semipublic
-
1
def initialize_serial(resource, next_id)
-
return unless serial = resource.model.serial(name)
-
return unless serial.get!(resource).nil?
-
serial.set!(resource, next_id)
-
-
# TODO: replace above with this, once
-
# specs can handle random, non-sequential ids
-
#serial.set!(resource, rand(2**32))
-
end
-
-
# Translate the attributes into a Hash with the field as the key
-
#
-
# @example
-
# attributes = { User.properties[:name] => 'Dan Kubb' }
-
# adapter.attributes_as_fields(attributes) # => { 'name' => 'Dan Kubb' }
-
#
-
# @param [Hash] attributes
-
# the attributes with the Property as the key
-
#
-
# @return [Hash]
-
# the attributes with the Property#field as the key
-
#
-
# @api semipublic
-
1
def attributes_as_fields(attributes)
-
Hash[ attributes.map { |property, value| [ property.field, property.dump(value) ] } ]
-
end
-
-
1
private
-
-
# Initialize an AbstractAdapter instance
-
#
-
# @param [Symbol] name
-
# the adapter repository name
-
# @param [Hash] options
-
# the adapter configuration options
-
#
-
# @return [undefined]
-
#
-
# @api semipublic
-
1
def initialize(name, options)
-
1
@name = name
-
1
@options = options.dup.freeze
-
1
@resource_naming_convention = NamingConventions::Resource::UnderscoredAndPluralized
-
1
@field_naming_convention = NamingConventions::Field::Underscored
-
end
-
end # class AbstractAdapter
-
-
1
const_added(:AbstractAdapter)
-
end # module Adapters
-
end # module DataMapper
-
1
module DataMapper
-
1
module Associations
-
1
module ManyToMany #:nodoc:
-
1
class Relationship < Associations::OneToMany::Relationship
-
1
extend Chainable
-
-
1
OPTIONS = superclass::OPTIONS.dup << :through << :via
-
-
# Returns a set of keys that identify the target model
-
#
-
# @return [DataMapper::PropertySet]
-
# a set of properties that identify the target model
-
#
-
# @api semipublic
-
1
def child_key
-
6
return @child_key if defined?(@child_key)
-
-
2
repository_name = child_repository_name || parent_repository_name
-
2
properties = child_model.properties(repository_name)
-
-
2
@child_key = if @child_properties
-
child_key = properties.values_at(*@child_properties)
-
properties.class.new(child_key).freeze
-
else
-
2
properties.key
-
end
-
end
-
-
# @api semipublic
-
1
alias_method :target_key, :child_key
-
-
# Intermediate association for through model
-
# relationships
-
#
-
# Example: for :bugs association in
-
#
-
# class Software::Engineer
-
# include DataMapper::Resource
-
#
-
# has n, :missing_tests
-
# has n, :bugs, :through => :missing_tests
-
# end
-
#
-
# through is :missing_tests
-
#
-
# TODO: document a case when
-
# through option is a model and
-
# not an association name
-
#
-
# @api semipublic
-
1
def through
-
12
return @through if defined?(@through)
-
-
2
@through = options[:through]
-
-
2
if @through.kind_of?(Associations::Relationship)
-
return @through
-
end
-
-
2
model = source_model
-
2
repository_name = source_repository_name
-
2
relationships = model.relationships(repository_name)
-
2
name = through_relationship_name
-
-
2
@through = relationships[name] ||
-
DataMapper.repository(repository_name) do
-
2
model.has(min..max, name, through_model, one_to_many_options)
-
end
-
-
2
@through.child_key
-
-
2
@through
-
end
-
-
# @api semipublic
-
1
def via
-
6
return @via if defined?(@via)
-
-
2
@via = options[:via]
-
-
2
if @via.kind_of?(Associations::Relationship)
-
return @via
-
end
-
-
2
name = self.name
-
2
through = self.through
-
2
repository_name = through.relative_target_repository_name
-
2
through_model = through.target_model
-
2
relationships = through_model.relationships(repository_name)
-
2
singular_name = DataMapper::Inflector.singularize(name.to_s).to_sym
-
-
2
@via = relationships[@via] ||
-
relationships[name] ||
-
relationships[singular_name]
-
-
2
@via ||= if anonymous_through_model?
-
2
DataMapper.repository(repository_name) do
-
2
through_model.belongs_to(singular_name, target_model, many_to_one_options)
-
end
-
else
-
raise UnknownRelationshipError, "No relationships named #{name} or #{singular_name} in #{through_model}"
-
2
end
-
-
2
@via.child_key
-
-
2
@via
-
end
-
-
# @api semipublic
-
1
def links
-
2
return @links if defined?(@links)
-
-
2
@links = []
-
2
links = [ through, via ]
-
-
2
while relationship = links.shift
-
4
if relationship.respond_to?(:links)
-
links.unshift(*relationship.links)
-
else
-
4
@links << relationship
-
end
-
end
-
-
2
@links.freeze
-
end
-
-
# Initialize the chain for "many to many" relationships
-
#
-
# @api public
-
1
def finalize
-
4
through
-
4
via
-
end
-
-
# @api private
-
1
def source_scope(source)
-
4
{ through.inverse => source }
-
end
-
-
# @api private
-
1
def query
-
# TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
-
# returns the query supplied in the definition
-
17
@many_to_many_query ||= super.merge(:links => links).freeze
-
end
-
-
# Eager load the collection using the source as a base
-
#
-
# @param [Resource, Collection] source
-
# the source to query with
-
# @param [Query, Hash] other_query
-
# optional query to restrict the collection
-
#
-
# @return [ManyToMany::Collection]
-
# the loaded collection for the source
-
#
-
# @api private
-
1
def eager_load(source, other_query = nil)
-
# FIXME: enable SEL for m:m relationships
-
source.model.all(query_for(source, other_query))
-
end
-
-
1
private
-
-
# @api private
-
1
def through_model
-
4
namespace, name = through_model_namespace_name
-
-
4
if namespace.const_defined?(name)
-
3
namespace.const_get(name)
-
else
-
1
Model.new(name, namespace) do
-
# all properties added to the anonymous through model are keys
-
1
def property(name, type, options = {})
-
2
options[:key] = true
-
2
options.delete(:index)
-
2
super
-
end
-
end
-
end
-
end
-
-
# @api private
-
1
def through_model_namespace_name
-
6
target_parts = target_model.base_model.name.split('::')
-
6
source_parts = source_model.base_model.name.split('::')
-
-
6
name = [ target_parts.pop, source_parts.pop ].sort.join
-
-
6
namespace = Object
-
-
# find the common namespace between the target_model and source_model
-
6
target_parts.zip(source_parts) do |target_part, source_part|
-
break if target_part != source_part
-
namespace = namespace.const_get(target_part)
-
end
-
-
6
return namespace, name
-
end
-
-
# @api private
-
1
def through_relationship_name
-
2
if anonymous_through_model?
-
2
namespace = through_model_namespace_name.first
-
2
relationship_name = DataMapper::Inflector.underscore(through_model.name.sub(/\A#{namespace.name}::/, '')).tr('/', '_')
-
2
DataMapper::Inflector.pluralize(relationship_name).to_sym
-
else
-
options[:through]
-
end
-
end
-
-
# Check if the :through association uses an anonymous model
-
#
-
# An anonymous model means that DataMapper creates the model
-
# in-memory, and sets the relationships to join the source
-
# and the target model.
-
#
-
# @return [Boolean]
-
# true if the through model is anonymous
-
#
-
# @api private
-
1
def anonymous_through_model?
-
4
options[:through] == Resource
-
end
-
-
# @api private
-
1
def nearest_relationship
-
return @nearest_relationship if defined?(@nearest_relationship)
-
-
nearest_relationship = self
-
-
while nearest_relationship.respond_to?(:through)
-
nearest_relationship = nearest_relationship.through
-
end
-
-
@nearest_relationship = nearest_relationship
-
end
-
-
# @api private
-
1
def valid_target?(target)
-
relationship = via
-
source_key = relationship.source_key
-
target_key = relationship.target_key
-
-
target.kind_of?(target_model) &&
-
source_key.valid?(target_key.get(target))
-
end
-
-
# @api private
-
1
def valid_source?(source)
-
relationship = nearest_relationship
-
source_key = relationship.source_key
-
target_key = relationship.target_key
-
-
source.kind_of?(source_model) &&
-
target_key.valid?(source_key.get(source))
-
end
-
-
1
chainable do
-
# @api semipublic
-
1
def many_to_one_options
-
4
{ :parent_key => target_key.map { |property| property.name } }
-
end
-
-
# @api semipublic
-
1
def one_to_many_options
-
4
{ :parent_key => source_key.map { |property| property.name } }
-
end
-
end
-
-
# Returns the inverse relationship class
-
#
-
# @api private
-
1
def inverse_class
-
self.class
-
end
-
-
# @api private
-
1
def invert
-
inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
-
end
-
-
# @api private
-
1
def inverted_options
-
links = self.links.dup
-
through = links.pop.inverse
-
-
links.reverse_each do |relationship|
-
inverse = relationship.inverse
-
-
through = self.class.new(
-
inverse.name,
-
inverse.child_model,
-
inverse.parent_model,
-
inverse.options.merge(:through => through)
-
)
-
end
-
-
options = self.options
-
-
DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(
-
:through => through,
-
:child_key => options[:parent_key],
-
:parent_key => options[:child_key],
-
:inverse => self
-
)
-
end
-
-
# Returns collection class used by this type of
-
# relationship
-
#
-
# @api private
-
1
def collection_class
-
4
ManyToMany::Collection
-
end
-
end # class Relationship
-
-
1
class Collection < Associations::OneToMany::Collection
-
# Remove every Resource in the m:m Collection from the repository
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy
-
assert_source_saved 'The source must be saved before mass-deleting the collection'
-
-
# make sure the records are loaded so they can be found when
-
# the intermediaries are removed
-
lazy_load
-
-
unless intermediaries.all(via => self).destroy
-
return false
-
end
-
-
super
-
end
-
-
# Remove every Resource in the m:m Collection from the repository, bypassing validation
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection while skipping
-
# validation.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy!
-
assert_source_saved 'The source must be saved before mass-deleting the collection'
-
-
model = self.model
-
key = model.key(repository_name)
-
conditions = Query.target_conditions(self, key, key)
-
-
unless intermediaries.all(via => self).destroy!
-
return false
-
end
-
-
unless model.all(:repository => repository, :conditions => conditions).destroy!
-
return false
-
end
-
-
each do |resource|
-
resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
-
end
-
-
clear
-
-
true
-
end
-
-
# Return the intermediaries linking the source to the targets
-
#
-
# @return [Collection]
-
# the intermediary collection
-
#
-
# @api public
-
1
def intermediaries
-
through = self.through
-
source = self.source
-
-
@intermediaries ||= if through.loaded?(source)
-
through.get_collection(source)
-
else
-
reset_intermediaries
-
end
-
end
-
-
1
protected
-
-
# Map the resources in the collection to the intermediaries
-
#
-
# @return [Hash]
-
# the map of resources to their intermediaries
-
#
-
# @api private
-
1
def intermediary_for
-
@intermediary_for ||= {}
-
end
-
-
# @api private
-
1
def through
-
relationship.through
-
end
-
-
# @api private
-
1
def via
-
relationship.via
-
end
-
-
1
private
-
-
# @api private
-
1
def _create(attributes, execute_hooks = true)
-
via = self.via
-
if via.respond_to?(:resource_for)
-
resource = super
-
if create_intermediary(execute_hooks, resource)
-
resource
-
end
-
else
-
if intermediary = create_intermediary(execute_hooks)
-
super(attributes.merge(via.inverse => intermediary), execute_hooks)
-
end
-
end
-
end
-
-
# @api private
-
1
def _save(execute_hooks = true)
-
via = self.via
-
-
if @removed.any?
-
# delete only intermediaries linked to the removed targets
-
return false unless intermediaries.all(via => @removed).send(execute_hooks ? :destroy : :destroy!)
-
-
# reset the intermediaries so that it reflects the current state of the datastore
-
reset_intermediaries
-
end
-
-
loaded_entries = self.loaded_entries
-
-
if via.respond_to?(:resource_for)
-
super
-
loaded_entries.all? { |resource| create_intermediary(execute_hooks, resource) }
-
else
-
if loaded_entries.any? && (intermediary = create_intermediary(execute_hooks))
-
inverse = via.inverse
-
loaded_entries.each { |resource| inverse.set(resource, intermediary) }
-
end
-
-
super
-
end
-
end
-
-
# @api private
-
1
def create_intermediary(execute_hooks, resource = nil)
-
intermediary_for = self.intermediary_for
-
-
intermediary_resource = intermediary_for[resource]
-
return intermediary_resource if intermediary_resource
-
-
intermediaries = self.intermediaries
-
method = execute_hooks ? :save : :save!
-
-
return unless intermediaries.send(method)
-
-
attributes = {}
-
attributes[via] = resource if resource
-
-
intermediary = intermediaries.first_or_new(attributes)
-
return unless intermediary.__send__(method)
-
-
# map the resource, even if it is nil, to the intermediary
-
intermediary_for[resource] = intermediary
-
end
-
-
# @api private
-
1
def reset_intermediaries
-
through = self.through
-
source = self.source
-
-
through.set_collection(source, through.collection_for(source))
-
end
-
-
# @api private
-
1
def inverse_set(*)
-
# do nothing
-
end
-
end # class Collection
-
end # module ManyToMany
-
end # module Associations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Associations
-
1
module ManyToOne #:nodoc:
-
# Relationship class with implementation specific
-
# to n side of 1 to n association
-
1
class Relationship < Associations::Relationship
-
1
OPTIONS = superclass::OPTIONS.dup << :required << :key << :unique
-
-
# @api semipublic
-
1
alias_method :source_repository_name, :child_repository_name
-
-
# @api semipublic
-
1
alias_method :source_model, :child_model
-
-
# @api semipublic
-
1
alias_method :target_repository_name, :parent_repository_name
-
-
# @api semipublic
-
1
alias_method :target_model, :parent_model
-
-
# @api semipublic
-
1
alias_method :target_key, :parent_key
-
-
# @api semipublic
-
1
def required?
-
6
@required
-
end
-
-
# @api semipublic
-
1
def key?
-
6
@key
-
end
-
-
# @api semipublic
-
1
def unique?
-
!!@unique
-
end
-
-
# @deprecated
-
1
def nullable?
-
raise "#{self.class}#nullable? is deprecated, use #{self.class}#required? instead (#{caller.first})"
-
end
-
-
# Returns a set of keys that identify source model
-
#
-
# @return [DataMapper::PropertySet] a set of properties that identify source model
-
# @api private
-
1
def child_key
-
117
return @child_key if defined?(@child_key)
-
-
7
model = source_model
-
7
repository_name = source_repository_name || target_repository_name
-
7
properties = model.properties(repository_name)
-
-
7
source_key = target_key.zip(@child_properties || []).map do |target_property, property_name|
-
7
property_name ||= "#{name}_#{target_property.name}".to_sym
-
-
properties[property_name] || begin
-
# create the property within the correct repository
-
6
DataMapper.repository(repository_name) do
-
6
model.property(property_name, target_property.to_child_key, source_key_options(target_property))
-
end
-
7
end
-
end
-
-
7
@child_key = properties.class.new(source_key).freeze
-
end
-
-
# @api semipublic
-
1
alias_method :source_key, :child_key
-
-
# Initialize the foreign key property this "many to one"
-
# relationship uses to persist itself
-
#
-
# @api public
-
1
def finalize
-
35
child_key
-
end
-
-
# Returns a hash of conditions that scopes query that fetches
-
# target object
-
#
-
# @return [Hash]
-
# Hash of conditions that scopes query
-
#
-
# @api private
-
1
def source_scope(source)
-
if source.kind_of?(Resource)
-
Query.target_conditions(source, source_key, target_key)
-
else
-
super
-
end
-
end
-
-
# Returns a Resource for this relationship with a given source
-
#
-
# @param [Resource] source
-
# A Resource to scope the collection with
-
# @param [Query] other_query (optional)
-
# A Query to further scope the collection with
-
#
-
# @return [Resource]
-
# The resource scoped to the relationship, source and query
-
#
-
# @api private
-
1
def resource_for(source, other_query = nil)
-
query = query_for(source, other_query)
-
-
# If the target key is equal to the model key, we can use the
-
# Model#get so the IdentityMap is used
-
if target_key == target_model.key
-
target = target_model.get(*source_key.get!(source))
-
if query.conditions.matches?(target)
-
target
-
else
-
nil
-
end
-
else
-
target_model.first(query)
-
end
-
end
-
-
# Loads and returns association target (ex.: author) for given source resource
-
# (ex.: article)
-
#
-
# @param source [DataMapper::Resource]
-
# source object (ex.: instance of article)
-
# @param other_query [DataMapper::Query]
-
# Query options
-
#
-
# @api semipublic
-
1
def get(source, query = nil)
-
4
lazy_load(source)
-
-
4
if query
-
collection = get_collection(source)
-
collection.first(query) if collection
-
else
-
4
get!(source)
-
end
-
end
-
-
1
def get_collection(source)
-
target = get!(source)
-
target.collection_for_self if target
-
end
-
-
# Sets value of association target (ex.: author) for given source resource
-
# (ex.: article)
-
#
-
# @param source [DataMapper::Resource]
-
# source object (ex.: instance of article)
-
#
-
# @param target [DataMapper::Resource]
-
# target object (ex.: instance of author)
-
#
-
# @api semipublic
-
1
def set(source, target)
-
11
target = typecast(target)
-
11
source_key.set(source, target_key.get(target))
-
11
set!(source, target)
-
end
-
-
# @api semipublic
-
1
def default_for(source)
-
typecast(super)
-
end
-
-
# Loads association target and sets resulting value on
-
# given source resource
-
#
-
# @param [Resource] source
-
# the source resource for the association
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def lazy_load(source)
-
4
source_key_different = source_key_different?(source)
-
-
4
if (loaded?(source) && !source_key_different) || !valid_source?(source)
-
4
return
-
end
-
-
# SEL: load all related resources in the source collection
-
if source.saved? && (collection = source.collection).size > 1
-
eager_load(collection)
-
end
-
-
if !loaded?(source) || (source_key_dirty?(source) && source.saved?)
-
set!(source, resource_for(source))
-
elsif loaded?(source) && source_key_different
-
source_key.set(source, target_key.get!(get!(source)))
-
end
-
end
-
-
1
private
-
-
# Initializes the relationship, always using max cardinality of 1.
-
#
-
# @api semipublic
-
1
def initialize(name, source_model, target_model, options = {})
-
7
if options.key?(:nullable)
-
raise ":nullable is deprecated, use :required instead (#{caller[2]})"
-
end
-
-
7
@required = options.fetch(:required, true)
-
7
@key = options.fetch(:key, false)
-
7
@unique = options.fetch(:unique, false)
-
7
target_model ||= DataMapper::Inflector.camelize(name)
-
7
options = { :min => @required ? 1 : 0, :max => 1 }.update(options)
-
7
super
-
end
-
-
# Sets the association targets in the resource
-
#
-
# @param [Resource] source
-
# the source to set
-
# @param [Array(Resource)] targets
-
# the target resource for the association
-
# @param [Query, Hash] query
-
# not used
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def eager_load_targets(source, targets, query)
-
set(source, targets.first)
-
end
-
-
# @api private
-
1
def typecast(target)
-
11
if target.kind_of?(Hash)
-
target_model.new(target)
-
else
-
11
target
-
end
-
end
-
-
# Returns the inverse relationship class
-
#
-
# @api private
-
1
def inverse_class
-
OneToMany::Relationship
-
end
-
-
# Returns the inverse relationship name
-
#
-
# @api private
-
1
def inverse_name
-
name = super
-
return name if name
-
-
name = DataMapper::Inflector.demodulize(source_model.name)
-
name = DataMapper::Inflector.underscore(name)
-
name = DataMapper::Inflector.pluralize(name)
-
name.to_sym
-
end
-
-
# @api private
-
1
def source_key_options(target_property)
-
6
options = DataMapper::Ext::Hash.only(target_property.options, :length, :precision, :scale).update(
-
:index => name,
-
:required => required?,
-
:key => key?,
-
:unique => @unique
-
)
-
-
6
if target_property.primitive == Integer
-
6
min = target_property.min
-
6
max = target_property.max
-
-
6
options.update(:min => min, :max => max) if min && max
-
end
-
-
6
options
-
end
-
-
# @api private
-
1
def child_properties
-
110
source_key.map { |property| property.name }
-
end
-
-
# @api private
-
1
def source_key_different?(source)
-
4
source_key.get!(source) != target_key.get!(get!(source))
-
end
-
-
# @api private
-
1
def source_key_dirty?(source)
-
source.dirty_attributes.keys.any? { |property| source_key.include?(property) }
-
end
-
end # class Relationship
-
end # module ManyToOne
-
end # module Associations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Associations
-
1
module OneToMany #:nodoc:
-
1
class Relationship < Associations::Relationship
-
# @api semipublic
-
1
alias_method :target_repository_name, :child_repository_name
-
-
# @api semipublic
-
1
alias_method :target_model, :child_model
-
-
# @api semipublic
-
1
alias_method :source_repository_name, :parent_repository_name
-
-
# @api semipublic
-
1
alias_method :source_model, :parent_model
-
-
# @api semipublic
-
1
alias_method :source_key, :parent_key
-
-
# @api semipublic
-
1
def child_key
-
6
inverse.child_key
-
end
-
-
# @api semipublic
-
1
alias_method :target_key, :child_key
-
-
# Returns a Collection for this relationship with a given source
-
#
-
# @param [Resource] source
-
# A Resource to scope the collection with
-
# @param [Query] other_query (optional)
-
# A Query to further scope the collection with
-
#
-
# @return [Collection]
-
# The collection scoped to the relationship, source and query
-
#
-
# @api private
-
1
def collection_for(source, other_query = nil)
-
8
query = query_for(source, other_query)
-
-
8
collection = collection_class.new(query)
-
8
collection.relationship = self
-
8
collection.source = source
-
-
# make the collection empty if the source is new
-
8
collection.replace([]) if source.new?
-
-
8
collection
-
end
-
-
# Loads and returns association targets (ex.: articles) for given source resource
-
# (ex.: author)
-
#
-
# @api semipublic
-
1
def get(source, query = nil)
-
15
lazy_load(source)
-
15
collection = get_collection(source)
-
15
query ? collection.all(query) : collection
-
end
-
-
# @api private
-
1
def get_collection(source)
-
15
get!(source)
-
end
-
-
# Sets value of association targets (ex.: paragraphs) for given source resource
-
# (ex.: article)
-
#
-
# @api semipublic
-
1
def set(source, targets)
-
lazy_load(source)
-
get!(source).replace(targets)
-
end
-
-
# @api private
-
1
def set_collection(source, target)
-
set!(source, target)
-
end
-
-
# Loads association targets and sets resulting value on
-
# given source resource
-
#
-
# @param [Resource] source
-
# the source resource for the association
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def lazy_load(source)
-
15
return if loaded?(source)
-
-
# SEL: load all related resources in the source collection
-
8
if source.saved? && (collection = source.collection).size > 1
-
eager_load(collection)
-
end
-
-
8
unless loaded?(source)
-
8
set!(source, collection_for(source))
-
end
-
end
-
-
# initialize the inverse "many to one" relationships explicitly before
-
# initializing other relationships. This makes sure that foreign key
-
# properties always appear in the order they were declared.
-
#
-
# @api public
-
1
def finalize
-
8
child_model.relationships.each do |relationship|
-
# TODO: should this check #inverse?
-
# relationship.child_key if inverse?(relationship)
-
19
if relationship.kind_of?(Associations::ManyToOne::Relationship)
-
15
relationship.finalize
-
end
-
end
-
8
inverse.finalize
-
end
-
-
# @api semipublic
-
1
def default_for(source)
-
collection_for(source).replace(Array(super))
-
end
-
-
1
private
-
-
# @api semipublic
-
1
def initialize(name, target_model, source_model, options = {})
-
6
target_model ||= DataMapper::Inflector.camelize(DataMapper::Inflector.singularize(name.to_s))
-
6
options = { :min => 0, :max => source_model.n }.update(options)
-
6
super
-
end
-
-
# Sets the association targets in the resource
-
#
-
# @param [Resource] source
-
# the source to set
-
# @param [Array<Resource>] targets
-
# the target collection for the association
-
# @param [Query, Hash] query
-
# the query to scope the association with
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def eager_load_targets(source, targets, query)
-
set!(source, collection_for(source, query).set(targets))
-
end
-
-
# Returns collection class used by this type of
-
# relationship
-
#
-
# @api private
-
1
def collection_class
-
4
OneToMany::Collection
-
end
-
-
# Returns the inverse relationship class
-
#
-
# @api private
-
1
def inverse_class
-
8
ManyToOne::Relationship
-
end
-
-
# Returns the inverse relationship name
-
#
-
# @api private
-
1
def inverse_name
-
57
super || DataMapper::Inflector.underscore(DataMapper::Inflector.demodulize(source_model.name)).to_sym
-
end
-
-
# @api private
-
1
def child_properties
-
super || parent_key.map do |parent_property|
-
56
"#{inverse_name}_#{parent_property.name}".to_sym
-
56
end
-
end
-
end # class Relationship
-
-
1
class Collection < DataMapper::Collection
-
# @api private
-
1
attr_accessor :relationship
-
-
# @api private
-
1
attr_accessor :source
-
-
# @api public
-
1
def reload(*)
-
assert_source_saved 'The source must be saved before reloading the collection'
-
super
-
end
-
-
# Replace the Resources within the 1:m Collection
-
#
-
# @param [Enumerable] other
-
# List of other Resources to replace with
-
#
-
# @return [Collection]
-
# self
-
#
-
# @api public
-
1
def replace(*)
-
8
lazy_load # lazy load so that targets are always orphaned
-
8
super
-
end
-
-
# Removes all Resources from the 1:m Collection
-
#
-
# This should remove and orphan each Resource from the 1:m Collection.
-
#
-
# @return [Collection]
-
# self
-
#
-
# @api public
-
1
def clear
-
lazy_load # lazy load so that targets are always orphaned
-
super
-
end
-
-
# Update every Resource in the 1:m Collection
-
#
-
# @param [Hash] attributes
-
# attributes to update with
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update(*)
-
assert_source_saved 'The source must be saved before mass-updating the collection'
-
super
-
end
-
-
# Update every Resource in the 1:m Collection, bypassing validation
-
#
-
# @param [Hash] attributes
-
# attributes to update
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update!(*)
-
assert_source_saved 'The source must be saved before mass-updating the collection'
-
super
-
end
-
-
# Remove every Resource in the 1:m Collection from the repository
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy
-
assert_source_saved 'The source must be saved before mass-deleting the collection'
-
super
-
end
-
-
# Remove every Resource in the 1:m Collection from the repository, bypassing validation
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection while skipping
-
# validation.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy!
-
assert_source_saved 'The source must be saved before mass-deleting the collection'
-
super
-
end
-
-
1
private
-
-
# @api private
-
1
def _create(*)
-
assert_source_saved 'The source must be saved before creating a resource'
-
super
-
end
-
-
# @api private
-
1
def _save(execute_hooks = true)
-
assert_source_saved 'The source must be saved before saving the collection'
-
-
# update removed resources to not reference the source
-
@removed.all? { |resource| resource.destroyed? || resource.__send__(execute_hooks ? :save : :save!) } && super
-
end
-
-
# @api private
-
1
def lazy_load
-
31
if source.saved?
-
super
-
end
-
end
-
-
# @api private
-
1
def new_collection(query, resources = nil, &block)
-
collection = self.class.new(query, &block)
-
-
collection.relationship = relationship
-
collection.source = source
-
-
resources ||= filter(query) if loaded?
-
-
# set the resources after the relationship and source are set
-
if resources
-
collection.set(resources)
-
end
-
-
collection
-
end
-
-
# @api private
-
1
def resource_added(resource)
-
12
resource = initialize_resource(resource)
-
12
inverse_set(resource, source)
-
12
super
-
end
-
-
# @api private
-
1
def resource_removed(resource)
-
inverse_set(resource, nil)
-
super
-
end
-
-
# @api private
-
1
def inverse_set(source, target)
-
4
unless source.readonly?
-
4
relationship.inverse.set(source, target)
-
end
-
end
-
-
# @api private
-
1
def assert_source_saved(message)
-
unless source.saved?
-
raise UnsavedParentError, message
-
end
-
end
-
end # class Collection
-
end # module OneToMany
-
end # module Associations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Associations
-
1
module OneToOne #:nodoc:
-
1
class Relationship < Associations::Relationship
-
1
%w[ public protected private ].map do |visibility|
-
3
methods = superclass.send("#{visibility}_instance_methods", false) |
-
DataMapper::Subject.send("#{visibility}_instance_methods", false)
-
-
3
methods.each do |method|
-
55
undef_method method.to_sym unless method.to_s == 'initialize'
-
end
-
end
-
-
# Loads (if necessary) and returns association target
-
# for given source
-
#
-
# @api semipublic
-
1
def get(source, query = nil)
-
relationship.get(source, query).first
-
end
-
-
# Get the resource directly
-
#
-
# @api semipublic
-
1
def get!(source)
-
collection = relationship.get!(source)
-
collection.first if collection
-
end
-
-
# Sets and returns association target
-
# for given source
-
#
-
# @api semipublic
-
1
def set(source, target)
-
relationship.set(source, [ target ].compact).first
-
end
-
-
# Sets the resource directly
-
#
-
# @api semipublic
-
1
def set!(source, target)
-
set(source, target)
-
end
-
-
# @api semipublic
-
1
def default_for(source)
-
relationship.default_for(source).first
-
end
-
-
# @api public
-
1
def kind_of?(klass)
-
super || relationship.kind_of?(klass)
-
end
-
-
# @api public
-
1
def instance_of?(klass)
-
super || relationship.instance_of?(klass)
-
end
-
-
# @api public
-
1
def respond_to?(method, include_private = false)
-
super || relationship.respond_to?(method, include_private)
-
end
-
-
1
private
-
-
1
attr_reader :relationship
-
-
# Initializes the relationship. Always assumes target model class is
-
# a camel cased association name.
-
#
-
# @api semipublic
-
1
def initialize(name, target_model, source_model, options = {})
-
klass = options.key?(:through) ? ManyToMany::Relationship : OneToMany::Relationship
-
target_model ||= DataMapper::Inflector.camelize(name).freeze
-
@relationship = klass.new(name, target_model, source_model, options)
-
end
-
-
# @api private
-
1
def method_missing(method, *args, &block)
-
relationship.send(method, *args, &block)
-
end
-
end # class Relationship
-
end # module HasOne
-
end # module Associations
-
end # module DataMapper
-
# TODO: move argument and option validation into the class
-
-
1
module DataMapper
-
1
module Associations
-
# Base class for relationships. Each type of relationship
-
# (1 to 1, 1 to n, n to m) implements a subclass of this class
-
# with methods like get and set overridden.
-
1
class Relationship
-
1
include DataMapper::Assertions
-
1
include Subject
-
-
1
OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse, :reader_visibility, :writer_visibility, :default ].to_set
-
-
# Relationship name
-
#
-
# @example for :parent association in
-
#
-
# class VersionControl::Commit
-
# # ...
-
#
-
# belongs_to :parent
-
# end
-
#
-
# name is :parent
-
#
-
# @api semipublic
-
1
attr_reader :name
-
-
# Options used to set up association of this relationship
-
#
-
# @example for :author association in
-
#
-
# class VersionControl::Commit
-
# # ...
-
#
-
# belongs_to :author, :model => 'Person'
-
# end
-
#
-
# options is a hash with a single key, :model
-
#
-
# @api semipublic
-
1
attr_reader :options
-
-
# ivar used to store collection of child options in source
-
#
-
# @example for :commits association in
-
#
-
# class VersionControl::Branch
-
# # ...
-
#
-
# has n, :commits
-
# end
-
#
-
# instance variable name for source will be @commits
-
#
-
# @api semipublic
-
1
attr_reader :instance_variable_name
-
-
# Repository from where child objects are loaded
-
#
-
# @api semipublic
-
1
attr_reader :child_repository_name
-
-
# Repository from where parent objects are loaded
-
#
-
# @api semipublic
-
1
attr_reader :parent_repository_name
-
-
# Minimum number of child objects for relationship
-
#
-
# @example for :cores association in
-
#
-
# class CPU::Multicore
-
# # ...
-
#
-
# has 2..n, :cores
-
# end
-
#
-
# minimum is 2
-
#
-
# @api semipublic
-
1
attr_reader :min
-
-
# Maximum number of child objects for
-
# relationship
-
#
-
# @example for :fouls association in
-
#
-
# class Basketball::Player
-
# # ...
-
#
-
# has 0..5, :fouls
-
# end
-
#
-
# maximum is 5
-
#
-
# @api semipublic
-
1
attr_reader :max
-
-
# Returns the visibility for the source accessor
-
#
-
# @return [Symbol]
-
# the visibility for the accessor added to the source
-
#
-
# @api semipublic
-
1
attr_reader :reader_visibility
-
-
# Returns the visibility for the source mutator
-
#
-
# @return [Symbol]
-
# the visibility for the mutator added to the source
-
#
-
# @api semipublic
-
1
attr_reader :writer_visibility
-
-
# Returns query options for relationship.
-
#
-
# For this base class, always returns query options
-
# has been initialized with.
-
# Overriden in subclasses.
-
#
-
# @api private
-
1
attr_reader :query
-
-
# Returns the String the Relationship would use in a Hash
-
#
-
# @return [String]
-
# String name for the Relationship
-
#
-
# @api private
-
1
def field
-
name.to_s
-
end
-
-
# Returns a hash of conditions that scopes query that fetches
-
# target object
-
#
-
# @return [Hash]
-
# Hash of conditions that scopes query
-
#
-
# @api private
-
1
def source_scope(source)
-
4
{ inverse => source }
-
end
-
-
# Creates and returns Query instance that fetches
-
# target resource(s) (ex.: articles) for given target resource (ex.: author)
-
#
-
# @api semipublic
-
1
def query_for(source, other_query = nil)
-
8
repository_name = relative_target_repository_name_for(source)
-
-
8
DataMapper.repository(repository_name).scope do
-
8
query = target_model.query.dup
-
8
query.update(self.query)
-
8
query.update(:conditions => source_scope(source))
-
8
query.update(other_query) if other_query
-
8
query.update(:fields => query.fields | target_key)
-
end
-
end
-
-
# Returns model class used by child side of the relationship
-
#
-
# @return [Resource]
-
# Model for association child
-
#
-
# @api private
-
1
def child_model
-
161
return @child_model if defined?(@child_model)
-
4
child_model_name = self.child_model_name
-
4
@child_model = DataMapper::Ext::Module.find_const(@parent_model || Object, child_model_name)
-
rescue NameError
-
raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}"
-
end
-
-
# @api private
-
1
def child_model?
-
6
child_model
-
6
true
-
rescue NameError
-
false
-
end
-
-
# @api private
-
1
def child_model_name
-
4
@child_model ? child_model.name : @child_model_name
-
end
-
-
# Returns a set of keys that identify the target model
-
#
-
# @return [PropertySet]
-
# a set of properties that identify the target model
-
#
-
# @api semipublic
-
1
def child_key
-
return @child_key if defined?(@child_key)
-
-
repository_name = child_repository_name || parent_repository_name
-
properties = child_model.properties(repository_name)
-
-
@child_key = if @child_properties
-
child_key = properties.values_at(*@child_properties)
-
properties.class.new(child_key).freeze
-
else
-
properties.key
-
end
-
end
-
-
# Access Relationship#child_key directly
-
#
-
# @api private
-
1
alias_method :relationship_child_key, :child_key
-
1
private :relationship_child_key
-
-
# Returns model class used by parent side of the relationship
-
#
-
# @return [Resource]
-
# Class of association parent
-
#
-
# @api private
-
1
def parent_model
-
196
return @parent_model if defined?(@parent_model)
-
4
parent_model_name = self.parent_model_name
-
4
@parent_model = DataMapper::Ext::Module.find_const(@child_model || Object, parent_model_name)
-
rescue NameError
-
raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}"
-
end
-
-
# @api private
-
1
def parent_model?
-
6
parent_model
-
6
true
-
rescue NameError
-
false
-
end
-
-
# @api private
-
1
def parent_model_name
-
4
@parent_model ? parent_model.name : @parent_model_name
-
end
-
-
# Returns a set of keys that identify parent model
-
#
-
# @return [PropertySet]
-
# a set of properties that identify parent model
-
#
-
# @api private
-
1
def parent_key
-
80
return @parent_key if defined?(@parent_key)
-
-
13
repository_name = parent_repository_name || child_repository_name
-
13
properties = parent_model.properties(repository_name)
-
-
13
@parent_key = if @parent_properties
-
5
parent_key = properties.values_at(*@parent_properties)
-
5
properties.class.new(parent_key).freeze
-
else
-
8
properties.key
-
end
-
end
-
-
# Loads and returns "other end" of the association.
-
# Must be implemented in subclasses.
-
#
-
# @api semipublic
-
1
def get(resource, other_query = nil)
-
raise NotImplementedError, "#{self.class}#get not implemented"
-
end
-
-
# Gets "other end" of the association directly
-
# as @ivar on given resource. Subclasses usually
-
# use implementation of this class.
-
#
-
# @api semipublic
-
1
def get!(resource)
-
24
resource.instance_variable_get(instance_variable_name)
-
end
-
-
# Sets value of the "other end" of association
-
# on given resource. Must be implemented in subclasses.
-
#
-
# @api semipublic
-
1
def set(resource, association)
-
raise NotImplementedError, "#{self.class}#set not implemented"
-
end
-
-
# Sets "other end" of the association directly
-
# as @ivar on given resource. Subclasses usually
-
# use implementation of this class.
-
#
-
# @api semipublic
-
1
def set!(resource, association)
-
19
resource.instance_variable_set(instance_variable_name, association)
-
end
-
-
# Eager load the collection using the source as a base
-
#
-
# @param [Collection] source
-
# the source collection to query with
-
# @param [Query, Hash] query
-
# optional query to restrict the collection
-
#
-
# @return [Collection]
-
# the loaded collection for the source
-
#
-
# @api private
-
1
def eager_load(source, query = nil)
-
targets = source.model.all(query_for(source, query))
-
-
# FIXME: cannot associate targets to m:m collection yet
-
if source.loaded? && !source.kind_of?(ManyToMany::Collection)
-
associate_targets(source, targets)
-
end
-
-
targets
-
end
-
-
# Checks if "other end" of association is loaded on given
-
# resource.
-
#
-
# @api semipublic
-
1
def loaded?(resource)
-
199
resource.instance_variable_defined?(instance_variable_name)
-
end
-
-
# Test the resource to see if it is a valid target
-
#
-
# @param [Object] source
-
# the resource or collection to be tested
-
#
-
# @return [Boolean]
-
# true if the resource is valid
-
#
-
# @api semipulic
-
1
def valid?(value, negated = false)
-
case value
-
when Enumerable then valid_target_collection?(value, negated)
-
when Resource then valid_target?(value)
-
when nil then true
-
else
-
raise ArgumentError, "+value+ should be an Enumerable, Resource or nil, but was a #{value.class.name}"
-
end
-
end
-
-
# Compares another Relationship for equality
-
#
-
# @param [Relationship] other
-
# the other Relationship to compare with
-
#
-
# @return [Boolean]
-
# true if they are equal, false if not
-
#
-
# @api public
-
1
def eql?(other)
-
return true if equal?(other)
-
instance_of?(other.class) && cmp?(other, :eql?)
-
end
-
-
# Compares another Relationship for equivalency
-
#
-
# @param [Relationship] other
-
# the other Relationship to compare with
-
#
-
# @return [Boolean]
-
# true if they are equal, false if not
-
#
-
# @api public
-
1
def ==(other)
-
15
return true if equal?(other)
-
other.respond_to?(:cmp_repository?, true) &&
-
15
other.respond_to?(:cmp_model?, true) &&
-
other.respond_to?(:cmp_key?, true) &&
-
other.respond_to?(:min) &&
-
other.respond_to?(:max) &&
-
other.respond_to?(:query) &&
-
cmp?(other, :==)
-
end
-
-
# Get the inverse relationship from the target model
-
#
-
# @api semipublic
-
1
def inverse
-
26
return @inverse if defined?(@inverse)
-
-
4
@inverse = options[:inverse]
-
-
4
if kind_of_inverse?(@inverse)
-
return @inverse
-
end
-
-
4
relationships = target_model.relationships(relative_target_repository_name)
-
-
7
@inverse = relationships.detect { |relationship| inverse?(relationship) } ||
-
invert
-
-
4
@inverse.child_key
-
-
4
@inverse
-
end
-
-
# @api private
-
1
def relative_target_repository_name
-
6
target_repository_name || source_repository_name
-
end
-
-
# @api private
-
1
def relative_target_repository_name_for(source)
-
8
target_repository_name || if source.respond_to?(:repository)
-
8
source.repository.name
-
else
-
source_repository_name
-
8
end
-
end
-
-
# @api private
-
1
def hash
-
self.class.hash ^
-
name.hash ^
-
child_repository_name.hash ^
-
parent_repository_name.hash ^
-
child_model.hash ^
-
parent_model.hash ^
-
child_properties.hash ^
-
parent_properties.hash ^
-
min.hash ^
-
max.hash ^
-
105
query.hash
-
end
-
-
1
private
-
-
# @api private
-
1
attr_reader :child_properties
-
-
# @api private
-
1
attr_reader :parent_properties
-
-
# Initializes new Relationship: sets attributes of relationship
-
# from options as well as conventions: for instance, @ivar name
-
# for association is constructed by prefixing @ to association name.
-
#
-
# Once attributes are set, reader and writer are created for
-
# the resource association belongs to
-
#
-
# @api semipublic
-
1
def initialize(name, child_model, parent_model, options = {})
-
13
initialize_object_ivar('child_model', child_model)
-
13
initialize_object_ivar('parent_model', parent_model)
-
-
13
@name = name
-
13
@instance_variable_name = "@#{@name}".freeze
-
13
@options = options.dup.freeze
-
13
@child_repository_name = @options[:child_repository_name]
-
13
@parent_repository_name = @options[:parent_repository_name]
-
-
13
unless @options[:child_key].nil?
-
@child_properties = DataMapper::Ext.try_dup(@options[:child_key]).freeze
-
end
-
13
unless @options[:parent_key].nil?
-
5
@parent_properties = DataMapper::Ext.try_dup(@options[:parent_key]).freeze
-
end
-
-
13
@min = @options[:min]
-
13
@max = @options[:max]
-
13
@reader_visibility = @options.fetch(:reader_visibility, :public)
-
13
@writer_visibility = @options.fetch(:writer_visibility, :public)
-
13
@default = @options.fetch(:default, nil)
-
-
# TODO: normalize the @query to become :conditions => AndOperation
-
# - Property/Relationship/Path should be left alone
-
# - Symbol/String keys should become a Property, scoped to the target_repository and target_model
-
# - Extract subject (target) from Operator
-
# - subject should be processed same as above
-
# - each subject should be transformed into AbstractComparison
-
# object with the subject, operator and value
-
# - transform into an AndOperation object, and return the
-
# query as :condition => and_object from self.query
-
# - this should provide the best performance
-
-
13
@query = DataMapper::Ext::Hash.except(@options, *self.class::OPTIONS).freeze
-
end
-
-
# Set the correct ivars for the named object
-
#
-
# This method should set the object in an ivar with the same name
-
# provided, plus it should set a String form of the object in
-
# a second ivar.
-
#
-
# @param [String]
-
# the name of the ivar to set
-
# @param [#name, #to_str, #to_sym] object
-
# the object to set in the ivar
-
#
-
# @return [String]
-
# the String value
-
#
-
# @raise [ArgumentError]
-
# raise when object does not respond to expected methods
-
#
-
# @api private
-
1
def initialize_object_ivar(name, object)
-
44
if object.respond_to?(:name)
-
18
instance_variable_set("@#{name}", object)
-
18
initialize_object_ivar(name, object.name)
-
26
elsif object.respond_to?(:to_str)
-
26
instance_variable_set("@#{name}_name", object.to_str.dup.freeze)
-
elsif object.respond_to?(:to_sym)
-
instance_variable_set("@#{name}_name", object.to_sym)
-
else
-
raise ArgumentError, "#{name} does not respond to #to_str or #name"
-
end
-
-
44
object
-
end
-
-
# Sets the association targets in the resource
-
#
-
# @param [Resource] source
-
# the source to set
-
# @param [Array<Resource>] targets
-
# the targets for the association
-
# @param [Query, Hash] query
-
# the query to scope the association with
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def eager_load_targets(source, targets, query)
-
raise NotImplementedError, "#{self.class}#eager_load_targets not implemented"
-
end
-
-
# @api private
-
1
def valid_target_collection?(collection, negated)
-
if collection.kind_of?(Collection)
-
# TODO: move the check for model_key into Collection#reloadable?
-
# since what we're really checking is a Collection's ability
-
# to reload itself, which is (currently) only possible if the
-
# key was loaded.
-
model = target_model
-
model_key = model.key(repository.name)
-
-
collection.model <= model &&
-
(collection.query.fields & model_key) == model_key &&
-
(collection.loaded? ? (collection.any? || negated) : true)
-
else
-
collection.all? { |resource| valid_target?(resource) }
-
end
-
end
-
-
# @api private
-
1
def valid_target?(target)
-
target.kind_of?(target_model) &&
-
source_key.valid?(target_key.get(target))
-
end
-
-
# @api private
-
1
def valid_source?(source)
-
source.kind_of?(source_model) &&
-
target_key.valid?(source_key.get(source))
-
end
-
-
# @api private
-
1
def inverse?(other)
-
3
return true if @inverse.equal?(other)
-
-
other != self &&
-
3
kind_of_inverse?(other) &&
-
cmp_repository?(other, :==, :child) &&
-
cmp_repository?(other, :==, :parent) &&
-
cmp_model?(other, :==, :child) &&
-
cmp_model?(other, :==, :parent) &&
-
cmp_key?(other, :==, :child) &&
-
cmp_key?(other, :==, :parent)
-
-
# TODO: match only when the Query is empty, or is the same as the
-
# default scope for the target model
-
end
-
-
# @api private
-
1
def inverse_name
-
57
inverse = options[:inverse]
-
57
if inverse.kind_of?(Relationship)
-
inverse.name
-
else
-
57
inverse
-
end
-
end
-
-
# @api private
-
1
def invert
-
1
inverse_class.new(inverse_name, child_model, parent_model, inverted_options)
-
end
-
-
# @api private
-
1
def inverted_options
-
1
DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(:inverse => self)
-
end
-
-
# @api private
-
1
def kind_of_inverse?(other)
-
7
other.kind_of?(inverse_class)
-
end
-
-
# @api private
-
1
def cmp?(other, operator)
-
name.send(operator, other.name) &&
-
15
cmp_repository?(other, operator, :child) &&
-
cmp_repository?(other, operator, :parent) &&
-
cmp_model?(other, operator, :child) &&
-
cmp_model?(other, operator, :parent) &&
-
cmp_key?(other, operator, :child) &&
-
cmp_key?(other, operator, :parent) &&
-
min.send(operator, other.min) &&
-
max.send(operator, other.max) &&
-
query.send(operator, other.query)
-
end
-
-
# @api private
-
1
def cmp_repository?(other, operator, type)
-
# if either repository is nil, then the relationship is relative,
-
# and the repositories are considered equivalent
-
6
return true unless repository_name = send("#{type}_repository_name")
-
3
return true unless other_repository_name = other.send("#{type}_repository_name")
-
-
repository_name.send(operator, other_repository_name)
-
end
-
-
# @api private
-
1
def cmp_model?(other, operator, type)
-
send("#{type}_model?") &&
-
6
other.send("#{type}_model?") &&
-
send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model)
-
end
-
-
# @api private
-
1
def cmp_key?(other, operator, type)
-
6
property_method = "#{type}_properties"
-
-
6
self_key = send(property_method)
-
6
other_key = other.send(property_method)
-
-
6
self_key.send(operator, other_key)
-
end
-
-
1
def associate_targets(source, targets)
-
# TODO: create an object that wraps this logic, and when the first
-
# kicker is fired, then it'll load up the collection, and then
-
# populate all the other methods
-
-
target_maps = Hash.new { |hash, key| hash[key] = [] }
-
-
targets.each do |target|
-
target_maps[target_key.get(target)] << target
-
end
-
-
Array(source).each do |source|
-
key = source_key.get(source)
-
eager_load_targets(source, target_maps[key], query)
-
end
-
end
-
end # class Relationship
-
end # module Associations
-
end # module DataMapper
-
1
require "dm-core/support/deprecate"
-
-
1
module DataMapper
-
1
module Resource
-
1
extend Deprecate
-
-
1
deprecate :persisted_state, :persistence_state
-
1
deprecate :persisted_state=, :persistence_state=
-
1
deprecate :persisted_state?, :persistence_state?
-
-
end # module Resource
-
-
end # module DataMapper
-
# TODO: if Collection is scoped by a unique property, should adding
-
# new Resources be denied?
-
-
# TODO: add #copy method
-
-
# TODO: move Collection#loaded_entries to LazyArray
-
# TODO: move Collection#partially_loaded to LazyArray
-
-
1
module DataMapper
-
# The Collection class represents a list of resources persisted in
-
# a repository and identified by a query.
-
#
-
# A Collection should act like an Array in every way, except that
-
# it will attempt to defer loading until the results from the
-
# repository are needed.
-
#
-
# A Collection is typically returned by the Model#all
-
# method.
-
1
class Collection < LazyArray
-
-
# Returns the Query the Collection is scoped with
-
#
-
# @return [Query]
-
# the Query the Collection is scoped with
-
#
-
# @api semipublic
-
1
attr_reader :query
-
-
# Returns the Repository
-
#
-
# @return [Repository]
-
# the Repository this Collection is associated with
-
#
-
# @api semipublic
-
1
def repository
-
8
query.repository
-
end
-
-
# Returns the Model
-
#
-
# @return [Model]
-
# the Model the Collection is associated with
-
#
-
# @api semipublic
-
1
def model
-
12
query.model
-
end
-
-
# Reloads the Collection from the repository
-
#
-
# If +query+ is provided, updates this Collection's query with its conditions
-
#
-
# cars_from_91 = Cars.all(:year_manufactured => 1991)
-
# cars_from_91.first.year_manufactured = 2001 # note: not saved
-
# cars_from_91.reload
-
# cars_from_91.first.year #=> 1991
-
#
-
# @param [Query, Hash] query (optional)
-
# further restrict results with query
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def reload(other_query = Undefined)
-
query = self.query
-
query = other_query.equal?(Undefined) ? query.dup : query.merge(other_query)
-
-
# make sure the Identity Map contains all the existing resources
-
identity_map = repository.identity_map(model)
-
-
loaded_entries.each do |resource|
-
identity_map[resource.key] = resource
-
end
-
-
# sort fields based on declared order, for more consistent reload queries
-
properties = self.properties
-
fields = properties & (query.fields | model_key | [ properties.discriminator ].compact)
-
-
# replace the list of resources
-
replace(all(query.update(:fields => fields, :reload => true)))
-
end
-
-
# Return the union with another collection
-
#
-
# @param [Collection] other
-
# the other collection
-
#
-
# @return [Collection]
-
# the union of the collection and other
-
#
-
# @api public
-
1
def union(other)
-
set_operation(:|, other)
-
end
-
-
1
alias_method :|, :union
-
1
alias_method :+, :union
-
-
# Return the intersection with another collection
-
#
-
# @param [Collection] other
-
# the other collection
-
#
-
# @return [Collection]
-
# the intersection of the collection and other
-
#
-
# @api public
-
1
def intersection(other)
-
set_operation(:&, other)
-
end
-
-
1
alias_method :&, :intersection
-
-
# Return the difference with another collection
-
#
-
# @param [Collection] other
-
# the other collection
-
#
-
# @return [Collection]
-
# the difference of the collection and other
-
#
-
# @api public
-
1
def difference(other)
-
set_operation(:-, other)
-
end
-
-
1
alias_method :-, :difference
-
-
# Lookup a Resource in the Collection by key
-
#
-
# This looksup a Resource by key, typecasting the key to the
-
# proper object if necessary.
-
#
-
# toyotas = Cars.all(:manufacturer => 'Toyota')
-
# toyo = Cars.first(:manufacturer => 'Toyota')
-
# toyotas.get(toyo.id) == toyo #=> true
-
#
-
# @param [Enumerable] *key
-
# keys which uniquely identify a resource in the Collection
-
#
-
# @return [Resource]
-
# Resource which matches the supplied key
-
# @return [nil]
-
# No Resource matches the supplied key
-
#
-
# @api public
-
1
def get(*key)
-
assert_valid_key_size(key)
-
-
key = model_key.typecast(key)
-
query = self.query
-
-
@identity_map[key] || if !loaded? && (query.limit || query.offset > 0)
-
# current query is exclusive, find resource within the set
-
-
# TODO: use a subquery to retrieve the Collection and then match
-
# it up against the key. This will require some changes to
-
# how subqueries are generated, since the key may be a
-
# composite key. In the case of DO adapters, it means subselects
-
# like the form "(a, b) IN(SELECT a, b FROM ...)", which will
-
# require making it so the Query condition key can be a
-
# Property or an Array of Property objects
-
-
# use the brute force approach until subquery lookups work
-
lazy_load
-
@identity_map[key]
-
else
-
# current query is all inclusive, lookup using normal approach
-
first(model.key_conditions(repository, key).update(:order => nil))
-
end
-
end
-
-
# Lookup a Resource in the Collection by key, raising an exception if not found
-
#
-
# This looksup a Resource by key, typecasting the key to the
-
# proper object if necessary.
-
#
-
# @param [Enumerable] *key
-
# keys which uniquely identify a resource in the Collection
-
#
-
# @return [Resource]
-
# Resource which matches the supplied key
-
# @return [nil]
-
# No Resource matches the supplied key
-
#
-
# @raise [ObjectNotFoundError] Resource could not be found by key
-
#
-
# @api public
-
1
def get!(*key)
-
get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect}")
-
end
-
-
# Returns a new Collection optionally scoped by +query+
-
#
-
# This returns a new Collection scoped relative to the current
-
# Collection.
-
#
-
# cars_from_91 = Cars.all(:year_manufactured => 1991)
-
# toyotas_91 = cars_from_91.all(:manufacturer => 'Toyota')
-
# toyotas_91.all? { |car| car.year_manufactured == 1991 } #=> true
-
# toyotas_91.all? { |car| car.manufacturer == 'Toyota' } #=> true
-
#
-
# If +query+ is a Hash, results will be found by merging +query+ with this Collection's query.
-
# If +query+ is a Query, results will be found using +query+ as an absolute query.
-
#
-
# @param [Hash, Query] query
-
# optional parameters to scope results with
-
#
-
# @return [Collection]
-
# Collection scoped by +query+
-
#
-
# @api public
-
1
def all(query = Undefined)
-
if query.equal?(Undefined) || (query.kind_of?(Hash) && query.empty?)
-
dup
-
else
-
# TODO: if there is no order parameter, and the Collection is not loaded
-
# check to see if the query can be satisfied by the head/tail
-
new_collection(scoped_query(query))
-
end
-
end
-
-
# Return the first Resource or the first N Resources in the Collection with an optional query
-
#
-
# When there are no arguments, return the first Resource in the
-
# Collection. When the first argument is an Integer, return a
-
# Collection containing the first N Resources. When the last
-
# (optional) argument is a Hash scope the results to the query.
-
#
-
# @param [Integer] limit (optional)
-
# limit the returned Collection to a specific number of entries
-
# @param [Hash] query (optional)
-
# scope the returned Resource or Collection to the supplied query
-
#
-
# @return [Resource, Collection]
-
# The first resource in the entries of this collection,
-
# or a new collection whose query has been merged
-
#
-
# @api public
-
1
def first(*args)
-
first_arg = args.first
-
last_arg = args.last
-
-
limit_specified = first_arg.kind_of?(Integer)
-
with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
-
-
limit = limit_specified ? first_arg : 1
-
query = with_query ? last_arg : {}
-
-
query = self.query.slice(0, limit).update(query)
-
-
# TODO: when a query provided, and there are enough elements in head to
-
# satisfy the query.limit, filter the head with the query, and make
-
# sure it matches the limit exactly. if so, use that result instead
-
# of calling all()
-
# - this can probably only be done if there is no :order parameter
-
-
loaded = loaded?
-
head = self.head
-
-
collection = if !with_query && (loaded || lazy_possible?(head, limit))
-
new_collection(query, super(limit))
-
else
-
all(query)
-
end
-
-
return collection if limit_specified
-
-
resource = collection.to_a.first
-
-
if with_query || loaded
-
resource
-
elsif resource
-
head[0] = resource
-
end
-
end
-
-
# Return the last Resource or the last N Resources in the Collection with an optional query
-
#
-
# When there are no arguments, return the last Resource in the
-
# Collection. When the first argument is an Integer, return a
-
# Collection containing the last N Resources. When the last
-
# (optional) argument is a Hash scope the results to the query.
-
#
-
# @param [Integer] limit (optional)
-
# limit the returned Collection to a specific number of entries
-
# @param [Hash] query (optional)
-
# scope the returned Resource or Collection to the supplied query
-
#
-
# @return [Resource, Collection]
-
# The last resource in the entries of this collection,
-
# or a new collection whose query has been merged
-
#
-
# @api public
-
1
def last(*args)
-
first_arg = args.first
-
last_arg = args.last
-
-
limit_specified = first_arg.kind_of?(Integer)
-
with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
-
-
limit = limit_specified ? first_arg : 1
-
query = with_query ? last_arg : {}
-
-
query = self.query.slice(0, limit).update(query).reverse!
-
-
# tell the Query to prepend each result from the adapter
-
query.update(:add_reversed => !query.add_reversed?)
-
-
# TODO: when a query provided, and there are enough elements in tail to
-
# satisfy the query.limit, filter the tail with the query, and make
-
# sure it matches the limit exactly. if so, use that result instead
-
# of calling all()
-
-
loaded = loaded?
-
tail = self.tail
-
-
collection = if !with_query && (loaded || lazy_possible?(tail, limit))
-
new_collection(query, super(limit))
-
else
-
all(query)
-
end
-
-
return collection if limit_specified
-
-
resource = collection.to_a.last
-
-
if with_query || loaded
-
resource
-
elsif resource
-
tail[tail.empty? ? 0 : -1] = resource
-
end
-
end
-
-
# Lookup a Resource from the Collection by offset
-
#
-
# @param [Integer] offset
-
# offset of the Resource in the Collection
-
#
-
# @return [Resource]
-
# Resource which matches the supplied offset
-
# @return [nil]
-
# No Resource matches the supplied offset
-
#
-
# @api public
-
1
def at(offset)
-
if loaded? || partially_loaded?(offset)
-
super
-
elsif offset == 0
-
first
-
elsif offset > 0
-
first(:offset => offset)
-
elsif offset == -1
-
last
-
else
-
last(:offset => offset.abs - 1)
-
end
-
end
-
-
# Access LazyArray#slice directly
-
#
-
# Collection#[]= uses this to bypass Collection#slice and access
-
# the resources directly so that it can orphan them properly.
-
#
-
# @api private
-
1
alias_method :superclass_slice, :slice
-
1
private :superclass_slice
-
-
# Simulates Array#slice and returns a new Collection
-
# whose query has a new offset or limit according to the
-
# arguments provided.
-
#
-
# If you provide a range, the min is used as the offset
-
# and the max minues the offset is used as the limit.
-
#
-
# @param [Integer, Array(Integer), Range] *args
-
# the offset, offset and limit, or range indicating first and last position
-
#
-
# @return [Resource, Collection, nil]
-
# The entry which resides at that offset and limit,
-
# or a new Collection object with the set limits and offset
-
# @return [nil]
-
# The offset (or starting offset) is out of range
-
#
-
# @raise [ArgumentError] "arguments may be 1 or 2 Integers,
-
# or 1 Range object, was: #{args.inspect}"
-
#
-
# @api public
-
1
def [](*args)
-
offset, limit = extract_slice_arguments(*args)
-
-
if args.size == 1 && args.first.kind_of?(Integer)
-
return at(offset)
-
end
-
-
query = sliced_query(offset, limit)
-
-
if loaded? || partially_loaded?(offset, limit)
-
new_collection(query, super)
-
else
-
new_collection(query)
-
end
-
end
-
-
1
alias_method :slice, :[]
-
-
# Deletes and Returns the Resources given by an offset or a Range
-
#
-
# @param [Integer, Array(Integer), Range] *args
-
# the offset, offset and limit, or range indicating first and last position
-
#
-
# @return [Resource, Collection]
-
# The entry which resides at that offset and limit, or
-
# a new Collection object with the set limits and offset
-
# @return [Resource, Collection, nil]
-
# The offset is out of range
-
#
-
# @api public
-
1
def slice!(*args)
-
removed = super
-
-
resources_removed(removed) unless removed.nil?
-
-
# Workaround for Ruby <= 1.8.6
-
compact! if RUBY_VERSION <= '1.8.6'
-
-
unless removed.kind_of?(Enumerable)
-
return removed
-
end
-
-
offset, limit = extract_slice_arguments(*args)
-
-
query = sliced_query(offset, limit)
-
-
new_collection(query, removed)
-
end
-
-
# Splice a list of Resources at a given offset or range
-
#
-
# When nil is provided instead of a Resource or a list of Resources
-
# this will remove all of the Resources at the specified position.
-
#
-
# @param [Integer, Array(Integer), Range] *args
-
# The offset, offset and limit, or range indicating first and last position.
-
# The last argument may be a Resource, a list of Resources or nil.
-
#
-
# @return [Resource, Enumerable]
-
# the Resource or list of Resources that was spliced into the Collection
-
# @return [nil]
-
# If nil was used to delete the entries
-
#
-
# @api public
-
1
def []=(*args)
-
orphans = Array(superclass_slice(*args[0..-2]))
-
-
# relate new resources
-
resources = resources_added(super)
-
-
# mark resources as removed
-
resources_removed(orphans - loaded_entries)
-
-
resources
-
end
-
-
1
alias_method :splice, :[]=
-
-
# Return a copy of the Collection sorted in reverse
-
#
-
# @return [Collection]
-
# Collection equal to +self+ but ordered in reverse
-
#
-
# @api public
-
1
def reverse
-
dup.reverse!
-
end
-
-
# Return the Collection sorted in reverse
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def reverse!
-
query.reverse!
-
-
# reverse without kicking if possible
-
if loaded?
-
@array.reverse!
-
else
-
# reverse and swap the head and tail
-
@head, @tail = tail.reverse!, head.reverse!
-
end
-
-
self
-
end
-
-
# Iterate over each Resource
-
#
-
# @yield [Resource] Each resource in the collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def each
-
9
return to_enum unless block_given?
-
9
super do |resource|
-
2
begin
-
2
original, resource.collection = resource.collection, self
-
2
yield resource
-
ensure
-
2
resource.collection = original
-
end
-
end
-
end
-
-
# Invoke the block for each resource and replace it the return value
-
#
-
# @yield [Resource] Each resource in the collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def collect!
-
super { |resource| resource_added(yield(resource_removed(resource))) }
-
end
-
-
1
alias_method :map!, :collect!
-
-
# Append one Resource to the Collection and relate it
-
#
-
# @param [Resource] resource
-
# the resource to add to this collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def <<(resource)
-
12
super(resource_added(resource))
-
end
-
-
# Appends the resources to self
-
#
-
# @param [Enumerable] resources
-
# List of Resources to append to the collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def concat(resources)
-
super(resources_added(resources))
-
end
-
-
# Append one or more Resources to the Collection
-
#
-
# This should append one or more Resources to the Collection and
-
# relate each to the Collection.
-
#
-
# @param [Enumerable] *resources
-
# List of Resources to append
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def push(*resources)
-
super(*resources_added(resources))
-
end
-
-
# Prepend one or more Resources to the Collection
-
#
-
# This should prepend one or more Resources to the Collection and
-
# relate each to the Collection.
-
#
-
# @param [Enumerable] *resources
-
# The Resources to prepend
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def unshift(*resources)
-
super(*resources_added(resources))
-
end
-
-
# Inserts the Resources before the Resource at the offset (which may be negative).
-
#
-
# @param [Integer] offset
-
# The offset to insert the Resources before
-
# @param [Enumerable] *resources
-
# List of Resources to insert
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def insert(offset, *resources)
-
super(offset, *resources_added(resources))
-
end
-
-
# Removes and returns the last Resource in the Collection
-
#
-
# @return [Resource]
-
# the last Resource in the Collection
-
#
-
# @api public
-
1
def pop(*)
-
if removed = super
-
resources_removed(removed)
-
end
-
end
-
-
# Removes and returns the first Resource in the Collection
-
#
-
# @return [Resource]
-
# the first Resource in the Collection
-
#
-
# @api public
-
1
def shift(*)
-
if removed = super
-
resources_removed(removed)
-
end
-
end
-
-
# Remove Resource from the Collection
-
#
-
# This should remove an included Resource from the Collection and
-
# orphan it from the Collection. If the Resource is not within the
-
# Collection, it should return nil.
-
#
-
# @param [Resource] resource the Resource to remove from
-
# the Collection
-
#
-
# @return [Resource]
-
# If +resource+ is within the Collection
-
# @return [nil]
-
# If +resource+ is not within the Collection
-
#
-
# @api public
-
1
def delete(resource)
-
if resource = super
-
resource_removed(resource)
-
end
-
end
-
-
# Remove Resource from the Collection by offset
-
#
-
# This should remove the Resource from the Collection at a given
-
# offset and orphan it from the Collection. If the offset is out of
-
# range return nil.
-
#
-
# @param [Integer] offset
-
# the offset of the Resource to remove from the Collection
-
#
-
# @return [Resource]
-
# If +offset+ is within the Collection
-
# @return [nil]
-
# If +offset+ is not within the Collection
-
#
-
# @api public
-
1
def delete_at(offset)
-
if resource = super
-
resource_removed(resource)
-
end
-
end
-
-
# Deletes every Resource for which block evaluates to true.
-
#
-
# @yield [Resource] Each resource in the Collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def delete_if
-
super { |resource| yield(resource) && resource_removed(resource) }
-
end
-
-
# Deletes every Resource for which block evaluates to true
-
#
-
# @yield [Resource] Each resource in the Collection
-
#
-
# @return [Collection]
-
# If resources were removed
-
# @return [nil]
-
# If no resources were removed
-
#
-
# @api public
-
1
def reject!
-
super { |resource| yield(resource) && resource_removed(resource) }
-
end
-
-
# Access LazyArray#replace directly
-
#
-
# @api private
-
1
alias_method :superclass_replace, :replace
-
1
private :superclass_replace
-
-
# Replace the Resources within the Collection
-
#
-
# @param [Enumerable] other
-
# List of other Resources to replace with
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def replace(other)
-
8
other = resources_added(other)
-
8
resources_removed(entries - other)
-
8
super(other)
-
end
-
-
# (Private) Set the Collection
-
#
-
# @param [Array] resources
-
# resources to add to the collection
-
#
-
# @return [self]
-
#
-
# @api private
-
1
def set(resources)
-
2
superclass_replace(resources_added(resources))
-
2
self
-
end
-
-
# Removes all Resources from the Collection
-
#
-
# This should remove and orphan each Resource from the Collection
-
#
-
# @return [self]
-
#
-
# @api public
-
1
def clear
-
if loaded?
-
resources_removed(self)
-
end
-
super
-
end
-
-
# Determines whether the collection is empty.
-
#
-
# @api public
-
1
alias_method :blank?, :empty?
-
-
# Finds the first Resource by conditions, or initializes a new
-
# Resource with the attributes if none found
-
#
-
# @param [Hash] conditions
-
# The conditions to be used to search
-
# @param [Hash] attributes
-
# The attributes to be used to initialize the resource with if none found
-
# @return [Resource]
-
# The instance found by +query+, or created with +attributes+ if none found
-
#
-
# @api public
-
1
def first_or_new(conditions = {}, attributes = {})
-
first(conditions) || new(conditions.merge(attributes))
-
end
-
-
# Finds the first Resource by conditions, or creates a new
-
# Resource with the attributes if none found
-
#
-
# @param [Hash] conditions
-
# The conditions to be used to search
-
# @param [Hash] attributes
-
# The attributes to be used to create the resource with if none found
-
# @return [Resource]
-
# The instance found by +query+, or created with +attributes+ if none found
-
#
-
# @api public
-
1
def first_or_create(conditions = {}, attributes = {})
-
first(conditions) || create(conditions.merge(attributes))
-
end
-
-
# Initializes a Resource and appends it to the Collection
-
#
-
# @param [Hash] attributes
-
# Attributes with which to initialize the new resource
-
#
-
# @return [Resource]
-
# a new Resource initialized with +attributes+
-
#
-
# @api public
-
1
def new(attributes = {})
-
resource = repository.scope { model.new(attributes) }
-
self << resource
-
resource
-
end
-
-
# Create a Resource in the Collection
-
#
-
# @param [Hash(Symbol => Object)] attributes
-
# attributes to set
-
#
-
# @return [Resource]
-
# the newly created Resource instance
-
#
-
# @api public
-
1
def create(attributes = {})
-
_create(attributes)
-
end
-
-
# Create a Resource in the Collection, bypassing hooks
-
#
-
# @param [Hash(Symbol => Object)] attributes
-
# attributes to set
-
#
-
# @return [Resource]
-
# the newly created Resource instance
-
#
-
# @api public
-
1
def create!(attributes = {})
-
_create(attributes, false)
-
end
-
-
# Update every Resource in the Collection
-
#
-
# Person.all(:age.gte => 21).update(:allow_beer => true)
-
#
-
# @param [Hash] attributes
-
# attributes to update with
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update(attributes)
-
assert_update_clean_only(:update)
-
-
dirty_attributes = model.new(attributes).dirty_attributes
-
dirty_attributes.empty? || all? { |resource| resource.update(attributes) }
-
end
-
-
# Update every Resource in the Collection bypassing validation
-
#
-
# Person.all(:age.gte => 21).update!(:allow_beer => true)
-
#
-
# @param [Hash] attributes
-
# attributes to update
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update!(attributes)
-
assert_update_clean_only(:update!)
-
-
model = self.model
-
-
dirty_attributes = model.new(attributes).dirty_attributes
-
-
if dirty_attributes.empty?
-
true
-
elsif dirty_attributes.any? { |property, value| !property.valid?(value) }
-
false
-
else
-
unless _update(dirty_attributes)
-
return false
-
end
-
-
if loaded?
-
each do |resource|
-
dirty_attributes.each { |property, value| property.set!(resource, value) }
-
repository.identity_map(model)[resource.key] = resource
-
end
-
end
-
-
true
-
end
-
end
-
-
# Save every Resource in the Collection
-
#
-
# @return [Boolean]
-
# true if the resources were successfully saved
-
#
-
# @api public
-
1
def save
-
_save
-
end
-
-
# Save every Resource in the Collection bypassing validation
-
#
-
# @return [Boolean]
-
# true if the resources were successfully saved
-
#
-
# @api public
-
1
def save!
-
_save(false)
-
end
-
-
# Remove every Resource in the Collection from the repository
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy
-
if destroyed = all? { |resource| resource.destroy }
-
clear
-
end
-
-
destroyed
-
end
-
-
# Remove all Resources from the repository, bypassing validation
-
#
-
# This performs a deletion of each Resource in the Collection from
-
# the repository and clears the Collection while skipping
-
# validation.
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy!
-
repository = self.repository
-
deleted = repository.delete(self)
-
-
if loaded?
-
unless deleted == size
-
return false
-
end
-
-
each do |resource|
-
resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
-
end
-
-
clear
-
else
-
mark_loaded
-
end
-
-
true
-
end
-
-
# Check to see if collection can respond to the method
-
#
-
# @param [Symbol] method
-
# method to check in the object
-
# @param [Boolean] include_private
-
# if set to true, collection will check private methods
-
#
-
# @return [Boolean]
-
# true if method can be responded to
-
#
-
# @api public
-
1
def respond_to?(method, include_private = false)
-
super || model.respond_to?(method) || relationships.named?(method)
-
end
-
-
# Checks if all the resources have no changes to save
-
#
-
# @return [Boolean]
-
# true if the resource may not be persisted
-
#
-
# @api public
-
1
def clean?
-
!dirty?
-
end
-
-
# Checks if any resources have unsaved changes
-
#
-
# @return [Boolean]
-
# true if the resources have unsaved changed
-
#
-
# @api public
-
1
def dirty?
-
loaded_entries.any? { |resource| resource.dirty? } || @removed.any?
-
end
-
-
# Gets a Human-readable representation of this collection,
-
# showing all elements contained in it
-
#
-
# @return [String]
-
# Human-readable representation of this collection, showing all elements
-
#
-
# @api public
-
1
def inspect
-
"[#{map { |resource| resource.inspect }.join(', ')}]"
-
end
-
-
# @api semipublic
-
1
def hash
-
self.class.hash ^ query.hash
-
end
-
-
1
protected
-
-
# Returns the model key
-
#
-
# @return [PropertySet]
-
# the model key
-
#
-
# @api private
-
1
def model_key
-
4
model.key(repository_name)
-
end
-
-
# Loaded Resources in the collection
-
#
-
# @return [Array<Resource>]
-
# Resources in the collection
-
#
-
# @api private
-
1
def loaded_entries
-
(loaded? ? self : head + tail).reject { |resource| resource.destroyed? }
-
end
-
-
# Returns the PropertySet representing the fields in the Collection scope
-
#
-
# @return [PropertySet]
-
# The set of properties this Collection's query will retrieve
-
#
-
# @api private
-
1
def properties
-
4
model.properties(repository_name)
-
end
-
-
# Returns the Relationships for the Collection's Model
-
#
-
# @return [Hash]
-
# The model's relationships, mapping the name to the
-
# Associations::Relationship object
-
#
-
# @api private
-
1
def relationships
-
model.relationships(repository_name)
-
end
-
-
1
private
-
-
# Initializes a new Collection identified by the query
-
#
-
# @param [Query] query
-
# Scope the results of the Collection
-
# @param [Enumerable] resources (optional)
-
# List of resources to initialize the Collection with
-
#
-
# @return [self]
-
#
-
# @api private
-
1
def initialize(query, resources = nil)
-
10
raise "#{self.class}#new with a block is deprecated" if block_given?
-
-
10
@query = query
-
10
@identity_map = IdentityMap.new
-
10
@removed = Set.new
-
-
10
super()
-
-
# TODO: change LazyArray to not use a load proc at all
-
10
remove_instance_variable(:@load_with_proc)
-
-
10
set(resources) if resources
-
end
-
-
# Copies the original Collection state
-
#
-
# @param [Collection] original
-
# the original collection to copy from
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def initialize_copy(original)
-
super
-
@query = @query.dup
-
@identity_map = @identity_map.dup
-
@removed = @removed.dup
-
end
-
-
# Initialize a resource from a Hash
-
#
-
# @param [Resource, Hash] resource
-
# resource to process
-
#
-
# @return [Resource]
-
# an initialized resource
-
#
-
# @api private
-
1
def initialize_resource(resource)
-
26
resource.kind_of?(Hash) ? new(resource) : resource
-
end
-
-
# Test if the collection is loaded between the offset and limit
-
#
-
# @param [Integer] offset
-
# the offset of the collection to test
-
# @param [Integer] limit
-
# optional limit for how many entries to be loaded
-
#
-
# @return [Boolean]
-
# true if the collection is loaded from the offset to the limit
-
#
-
# @api private
-
1
def partially_loaded?(offset, limit = 1)
-
if offset >= 0
-
lazy_possible?(head, offset + limit)
-
else
-
lazy_possible?(tail, offset.abs)
-
end
-
end
-
-
# Lazy loads a Collection
-
#
-
# @return [self]
-
#
-
# @api private
-
1
def lazy_load
-
if loaded?
-
return self
-
end
-
-
mark_loaded
-
-
head = self.head
-
tail = self.tail
-
query = self.query
-
-
resources = repository.read(query)
-
-
# remove already known results
-
resources -= head if head.any?
-
resources -= tail if tail.any?
-
resources -= @removed.to_a if @removed.any?
-
-
query.add_reversed? ? unshift(*resources.reverse) : concat(resources)
-
-
# TODO: DRY this up with LazyArray
-
@array.unshift(*head)
-
@array.concat(tail)
-
-
@head = @tail = nil
-
@reapers.each { |resource| @array.delete_if(&resource) } if @reapers
-
@array.freeze if frozen?
-
-
self
-
end
-
-
# Returns the Query Repository name
-
#
-
# @return [Symbol]
-
# the repository name
-
#
-
# @api private
-
1
def repository_name
-
8
repository.name
-
end
-
-
# Initializes a new Collection
-
#
-
# @return [Collection]
-
# A new Collection object
-
#
-
# @api private
-
1
def new_collection(query, resources = nil, &block)
-
if loaded?
-
resources ||= filter(query)
-
end
-
-
# TOOD: figure out a way to pass not-yet-saved Resources to this newly
-
# created Collection. If the new resource matches the conditions, then
-
# it should be added to the collection (keep in mind limit/offset too)
-
-
self.class.new(query, resources, &block)
-
end
-
-
# Apply a set operation on self and another collection
-
#
-
# @param [Symbol] operation
-
# the set operation to apply
-
# @param [Collection] other
-
# the other collection to apply the set operation on
-
#
-
# @return [Collection]
-
# the collection that was created for the set operation
-
#
-
# @api private
-
1
def set_operation(operation, other)
-
resources = set_operation_resources(operation, other)
-
other_query = Query.target_query(repository, model, other)
-
new_collection(query.send(operation, other_query), resources)
-
end
-
-
# Prepopulate the set operation if the collection is loaded
-
#
-
# @param [Symbol] operation
-
# the set operation to apply
-
# @param [Collection] other
-
# the other collection to apply the set operation on
-
#
-
# @return [nil]
-
# nil if the Collection is not loaded
-
# @return [Array]
-
# the resources to prepopulate the set operation results with
-
#
-
# @api private
-
1
def set_operation_resources(operation, other)
-
entries.send(operation, other.entries) if loaded?
-
end
-
-
# Creates a resource in the collection
-
#
-
# @param [Boolean] execute_hooks
-
# Whether to execute hooks or not
-
# @param [Hash] attributes
-
# Attributes with which to create the new resource
-
#
-
# @return [Resource]
-
# a saved Resource
-
#
-
# @api private
-
1
def _create(attributes, execute_hooks = true)
-
resource = repository.scope { model.send(execute_hooks ? :create : :create!, default_attributes.merge(attributes)) }
-
self << resource if resource.saved?
-
resource
-
end
-
-
# Updates a collection
-
#
-
# @return [Boolean]
-
# Returns true if collection was updated
-
#
-
# @api private
-
1
def _update(dirty_attributes)
-
repository.update(dirty_attributes, self)
-
true
-
end
-
-
# Saves a collection
-
#
-
# @param [Boolean] execute_hooks
-
# Whether to execute hooks or not
-
#
-
# @return [Boolean]
-
# Returns true if collection was updated
-
#
-
# @api private
-
1
def _save(execute_hooks = true)
-
loaded_entries = self.loaded_entries
-
loaded_entries.each { |resource| set_default_attributes(resource) }
-
@removed.clear
-
loaded_entries.all? { |resource| resource.__send__(execute_hooks ? :save : :save!) }
-
end
-
-
# Returns default values to initialize new Resources in the Collection
-
#
-
# @return [Hash] The default attributes for new instances in this Collection
-
#
-
# @api private
-
1
def default_attributes
-
4
return @default_attributes if @default_attributes
-
-
4
default_attributes = {}
-
-
4
conditions = query.conditions
-
-
4
if conditions.slug == :and
-
4
model_properties = properties.dup
-
4
model_key = self.model_key
-
-
4
if model_properties.to_set.superset?(model_key.to_set)
-
4
model_properties -= model_key
-
end
-
-
4
conditions.each do |condition|
-
4
next unless condition.slug == :eql
-
-
4
subject = condition.subject
-
4
next unless model_properties.include?(subject) || (condition.relationship? && subject.source_model == model)
-
-
4
default_attributes[subject] = condition.loaded_value
-
end
-
end
-
-
4
@default_attributes = default_attributes.freeze
-
end
-
-
# Set the default attributes for a non-frozen resource
-
#
-
# @param [Resource] resource
-
# the resource to set the default attributes for
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def set_default_attributes(resource)
-
4
unless resource.readonly?
-
4
resource.attributes = default_attributes
-
end
-
end
-
-
# Track the added resource
-
#
-
# @param [Resource] resource
-
# the resource that was added
-
#
-
# @return [Resource]
-
# the resource that was added
-
#
-
# @api private
-
1
def resource_added(resource)
-
14
resource = initialize_resource(resource)
-
-
14
if resource.saved?
-
10
@identity_map[resource.key] = resource
-
10
@removed.delete(resource)
-
else
-
4
set_default_attributes(resource)
-
end
-
-
14
resource
-
end
-
-
# Track the added resources
-
#
-
# @param [Array<Resource>] resources
-
# the resources that were added
-
#
-
# @return [Array<Resource>]
-
# the resources that were added
-
#
-
# @api private
-
1
def resources_added(resources)
-
10
if resources.kind_of?(Enumerable)
-
12
resources.map { |resource| resource_added(resource) }
-
else
-
resource_added(resources)
-
end
-
end
-
-
# Track the removed resource
-
#
-
# @param [Resource] resource
-
# the resource that was removed
-
#
-
# @return [Resource]
-
# the resource that was removed
-
#
-
# @api private
-
1
def resource_removed(resource)
-
if resource.saved?
-
@identity_map.delete(resource.key)
-
@removed << resource
-
end
-
-
resource
-
end
-
-
# Track the removed resources
-
#
-
# @param [Array<Resource>] resources
-
# the resources that were removed
-
#
-
# @return [Array<Resource>]
-
# the resources that were removed
-
#
-
# @api private
-
1
def resources_removed(resources)
-
8
if resources.kind_of?(Enumerable)
-
8
resources.each { |resource| resource_removed(resource) }
-
else
-
resource_removed(resources)
-
end
-
end
-
-
# Filter resources in the collection based on a Query
-
#
-
# @param [Query] query
-
# the query to match each resource in the collection
-
#
-
# @return [Array]
-
# the resources that match the Query
-
# @return [nil]
-
# nil if no resources match the Query
-
#
-
# @api private
-
1
def filter(other_query)
-
query = self.query
-
fields = query.fields.to_set
-
unique = other_query.unique?
-
-
# TODO: push this into a Query#subset? method
-
if other_query.links.empty? &&
-
(unique || (!unique && !query.unique?)) &&
-
!other_query.reload? &&
-
!other_query.raw? &&
-
other_query.fields.to_set.subset?(fields) &&
-
other_query.condition_properties.subset?(fields)
-
then
-
other_query.filter_records(to_a.dup)
-
end
-
end
-
-
# Return the absolute or relative scoped query
-
#
-
# @param [Query, Hash] query
-
# the query to scope the collection with
-
#
-
# @return [Query]
-
# the absolute or relative scoped query
-
#
-
# @api private
-
1
def scoped_query(query)
-
if query.kind_of?(Query)
-
query.dup
-
else
-
self.query.relative(query)
-
end
-
end
-
-
# @api private
-
1
def sliced_query(offset, limit)
-
query = self.query
-
-
if offset >= 0
-
query.slice(offset, limit)
-
else
-
query = query.slice((limit + offset).abs, limit).reverse!
-
-
# tell the Query to prepend each result from the adapter
-
query.update(:add_reversed => !query.add_reversed?)
-
end
-
end
-
-
# Delegates to Model, Relationships or the superclass (LazyArray)
-
#
-
# When this receives a method that belongs to the Model the
-
# Collection is scoped to, it will execute the method within the
-
# same scope as the Collection and return the results.
-
#
-
# When this receives a method that is a relationship the Model has
-
# defined, it will execute the association method within the same
-
# scope as the Collection and return the results.
-
#
-
# Otherwise this method will delegate to a method in the superclass
-
# (LazyArray) and return the results.
-
#
-
# @return [Object]
-
# the return values of the delegated methods
-
#
-
# @api public
-
1
def method_missing(method, *args, &block)
-
relationships = self.relationships
-
-
if model.respond_to?(method)
-
delegate_to_model(method, *args, &block)
-
elsif relationship = relationships[method] || relationships[DataMapper::Inflector.singularize(method.to_s).to_sym]
-
delegate_to_relationship(relationship, *args)
-
else
-
super
-
end
-
end
-
-
# Delegate the method to the Model
-
#
-
# @param [Symbol] method
-
# the name of the method in the model to execute
-
# @param [Array] *args
-
# the arguments for the method
-
#
-
# @return [Object]
-
# the return value of the model method
-
#
-
# @api private
-
1
def delegate_to_model(method, *args, &block)
-
model = self.model
-
model.send(:with_scope, query) do
-
model.send(method, *args, &block)
-
end
-
end
-
-
# Delegate the method to the Relationship
-
#
-
# @return [Collection]
-
# the associated Resources
-
#
-
# @api private
-
1
def delegate_to_relationship(relationship, query = nil)
-
relationship.eager_load(self, query)
-
end
-
-
# Raises an exception if #update is performed on a dirty resource
-
#
-
# @raise [UpdateConflictError]
-
# raise if the resource is dirty
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def assert_update_clean_only(method)
-
if dirty?
-
raise UpdateConflictError, "#{self.class}##{method} cannot be called on a dirty collection"
-
end
-
end
-
-
# Raises an exception if #get receives the wrong number of arguments
-
#
-
# @param [Array] key
-
# the key value
-
#
-
# @return [undefined]
-
#
-
# @raise [UpdateConflictError]
-
# raise if the resource is dirty
-
#
-
# @api private
-
1
def assert_valid_key_size(key)
-
expected_key_size = model_key.size
-
actual_key_size = key.size
-
-
if actual_key_size != expected_key_size
-
raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
-
end
-
end
-
end # class Collection
-
end # module DataMapper
-
1
module Kernel
-
-
# Returns the object's singleton class.
-
#
-
# @return [Class]
-
#
-
# @api private
-
def singleton_class
-
class << self
-
self
-
end
-
1
end unless respond_to?(:singleton_class) # exists in 1.9.2
-
-
1
private
-
-
# Delegates to DataMapper.repository()
-
#
-
# @api public
-
1
def repository(*args, &block)
-
DataMapper.repository(*args, &block)
-
end
-
-
end # module Kernel
-
1
class Pathname
-
# alias_method :to_s, :to to_str when to_str not defined
-
97
unless public_instance_methods(false).any? { |m| m.to_sym == :to_str }
-
1
alias_method :to_str, :to_s
-
end
-
end # class Pathname
-
1
class Symbol
-
1
(DataMapper::Query::Conditions::Comparison.slugs | [ :not, :asc, :desc ]).each do |sym|
-
11
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{sym}
-
#{"raise \"explicit use of '#{sym}' operator is deprecated (#{caller.first})\"" if sym == :eql || sym == :in}
-
DataMapper::Query::Operator.new(self, #{sym.inspect})
-
end
-
RUBY
-
end
-
end # class Symbol
-
1
module DataMapper
-
-
# Tracks objects to help ensure that each object gets loaded only once.
-
# See: http://www.martinfowler.com/eaaCatalog/identityMap.html
-
1
class IdentityMap < Hash
-
end # class IdentityMap
-
end # module DataMapper
-
1
module DataMapper
-
1
module Model
-
1
include Enumerable
-
-
1
WRITER_METHOD_REGEXP = /=\z/.freeze
-
1
INVALID_WRITER_METHODS = %w[ == != === []= taguri= attributes= collection= persistence_state= raise_on_save_failure= ].to_set.freeze
-
-
# Creates a new Model class with its constant already set
-
#
-
# If a block is passed, it will be eval'd in the context of the new Model
-
#
-
# @param [#to_s] name
-
# the name of the new model
-
# @param [Object] namespace
-
# the namespace that will hold the new model
-
# @param [Proc] block
-
# a block that will be eval'd in the context of the new Model class
-
#
-
# @return [Model]
-
# the newly created Model class
-
#
-
# @api private
-
1
def self.new(name = nil, namespace = Object, &block)
-
1
model = name ? namespace.const_set(name, Class.new) : Class.new
-
-
1
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
include DataMapper::Resource
-
RUBY
-
-
1
model.instance_eval(&block) if block
-
1
model
-
end
-
-
# Return all models that extend the Model module
-
#
-
# class Foo
-
# include DataMapper::Resource
-
# end
-
#
-
# DataMapper::Model.descendants.first #=> Foo
-
#
-
# @return [DescendantSet]
-
# Set containing the descendant models
-
#
-
# @api semipublic
-
1
def self.descendants
-
19
@descendants ||= DescendantSet.new
-
end
-
-
# Return all models that inherit from a Model
-
#
-
# class Foo
-
# include DataMapper::Resource
-
# end
-
#
-
# class Bar < Foo
-
# end
-
#
-
# Foo.descendants.first #=> Bar
-
#
-
# @return [Set]
-
# Set containing the descendant classes
-
#
-
# @api semipublic
-
1
def descendants
-
143
@descendants ||= DescendantSet.new
-
end
-
-
# Return if Resource#save should raise an exception on save failures (globally)
-
#
-
# This is false by default.
-
#
-
# DataMapper::Model.raise_on_save_failure # => false
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def self.raise_on_save_failure
-
24
if defined?(@raise_on_save_failure)
-
@raise_on_save_failure
-
else
-
24
false
-
end
-
end
-
-
# Specify if Resource#save should raise an exception on save failures (globally)
-
#
-
# @param [Boolean]
-
# a boolean that if true will cause Resource#save to raise an exception
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def self.raise_on_save_failure=(raise_on_save_failure)
-
@raise_on_save_failure = raise_on_save_failure
-
end
-
-
# Return if Resource#save should raise an exception on save failures (per-model)
-
#
-
# This delegates to DataMapper::Model.raise_on_save_failure by default.
-
#
-
# User.raise_on_save_failure # => false
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def raise_on_save_failure
-
24
if defined?(@raise_on_save_failure)
-
@raise_on_save_failure
-
else
-
24
DataMapper::Model.raise_on_save_failure
-
end
-
end
-
-
# Specify if Resource#save should raise an exception on save failures (per-model)
-
#
-
# @param [Boolean]
-
# a boolean that if true will cause Resource#save to raise an exception
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def raise_on_save_failure=(raise_on_save_failure)
-
@raise_on_save_failure = raise_on_save_failure
-
end
-
-
# Finish model setup and verify it is valid
-
#
-
# @return [undefined]
-
#
-
# @api public
-
1
def finalize
-
10
finalize_relationships
-
10
finalize_allowed_writer_methods
-
10
assert_valid_name
-
10
assert_valid_properties
-
10
assert_valid_key
-
end
-
-
# Appends a module for inclusion into the model class after Resource.
-
#
-
# This is a useful way to extend Resource while still retaining a
-
# self.included method.
-
#
-
# @param [Module] inclusions
-
# the module that is to be appended to the module after Resource
-
#
-
# @return [Boolean]
-
# true if the inclusions have been successfully appended to the list
-
#
-
# @api semipublic
-
1
def self.append_inclusions(*inclusions)
-
5
extra_inclusions.concat inclusions
-
-
# Add the inclusion to existing descendants
-
5
descendants.each do |model|
-
inclusions.each { |inclusion| model.send :include, inclusion }
-
end
-
-
5
true
-
end
-
-
# The current registered extra inclusions
-
#
-
# @return [Set]
-
#
-
# @api private
-
1
def self.extra_inclusions
-
10
@extra_inclusions ||= []
-
end
-
-
# Extends the model with this module after Resource has been included.
-
#
-
# This is a useful way to extend Model while still retaining a self.extended method.
-
#
-
# @param [Module] extensions
-
# List of modules that will extend the model after it is extended by Model
-
#
-
# @return [Boolean]
-
# whether or not the inclusions have been successfully appended to the list
-
#
-
# @api semipublic
-
1
def self.append_extensions(*extensions)
-
5
extra_extensions.concat extensions
-
-
# Add the extension to existing descendants
-
5
descendants.each do |model|
-
extensions.each { |extension| model.extend(extension) }
-
end
-
-
5
true
-
end
-
-
# The current registered extra extensions
-
#
-
# @return [Set]
-
#
-
# @api private
-
1
def self.extra_extensions
-
10
@extra_extensions ||= []
-
end
-
-
# @api private
-
1
def self.extended(descendant)
-
5
descendants << descendant
-
-
5
descendant.instance_variable_set(:@valid, false)
-
5
descendant.instance_variable_set(:@base_model, descendant)
-
5
descendant.instance_variable_set(:@storage_names, {})
-
5
descendant.instance_variable_set(:@default_order, {})
-
-
5
descendant.extend(Chainable)
-
-
35
extra_extensions.each { |mod| descendant.extend(mod) }
-
30
extra_inclusions.each { |mod| descendant.send(:include, mod) }
-
end
-
-
# @api private
-
1
def inherited(descendant)
-
descendants << descendant
-
-
descendant.instance_variable_set(:@valid, false)
-
descendant.instance_variable_set(:@base_model, base_model)
-
descendant.instance_variable_set(:@storage_names, @storage_names.dup)
-
descendant.instance_variable_set(:@default_order, @default_order.dup)
-
end
-
-
# Gets the name of the storage receptacle for this resource in the given
-
# Repository (ie., table name, for database stores).
-
#
-
# @return [String]
-
# the storage name (ie., table name, for database stores) associated with
-
# this resource in the given repository
-
#
-
# @api public
-
1
def storage_name(repository_name = default_repository_name)
-
125
storage_names[repository_name] ||= repository(repository_name).adapter.resource_naming_convention.call(default_storage_name).freeze
-
end
-
-
# the names of the storage receptacles for this resource across all repositories
-
#
-
# @return [Hash(Symbol => String)]
-
# All available names of storage receptacles
-
#
-
# @api public
-
1
def storage_names
-
125
@storage_names
-
end
-
-
# Grab a single record by its key. Supports natural and composite key
-
# lookups as well.
-
#
-
# Zoo.get(1) # get the zoo with primary key of 1.
-
# Zoo.get!(1) # Or get! if you want an ObjectNotFoundError on failure
-
# Zoo.get('DFW') # wow, support for natural primary keys
-
# Zoo.get('Metro', 'DFW') # more wow, composite key look-up
-
#
-
# @param [Object] *key
-
# The primary key or keys to use for lookup
-
#
-
# @return [Resource, nil]
-
# A single model that was found
-
# If no instance was found matching +key+
-
#
-
# @api public
-
1
def get(*key)
-
29
assert_valid_key_size(key)
-
-
29
repository = self.repository
-
29
key = self.key(repository.name).typecast(key)
-
-
29
repository.identity_map(self)[key] || first(key_conditions(repository, key).update(:order => nil))
-
end
-
-
# Grab a single record just like #get, but raise an ObjectNotFoundError
-
# if the record doesn't exist.
-
#
-
# @param [Object] *key
-
# The primary key or keys to use for lookup
-
# @return [Resource]
-
# A single model that was found
-
# @raise [ObjectNotFoundError]
-
# The record was not found
-
#
-
# @api public
-
1
def get!(*key)
-
get(*key) || raise(ObjectNotFoundError, "Could not find #{self.name} with key #{key.inspect}")
-
end
-
-
1
def [](*args)
-
all[*args]
-
end
-
-
1
alias_method :slice, :[]
-
-
1
def at(*args)
-
all.at(*args)
-
end
-
-
1
def fetch(*args, &block)
-
all.fetch(*args, &block)
-
end
-
-
1
def values_at(*args)
-
all.values_at(*args)
-
end
-
-
1
def reverse
-
all.reverse
-
end
-
-
1
def each(&block)
-
return to_enum unless block_given?
-
all.each(&block)
-
self
-
end
-
-
# Find a set of records matching an optional set of conditions. Additionally,
-
# specify the order that the records are return.
-
#
-
# Zoo.all # all zoos
-
# Zoo.all(:open => true) # all zoos that are open
-
# Zoo.all(:opened_on => start..end) # all zoos that opened on a date in the date-range
-
# Zoo.all(:order => [ :tiger_count.desc ]) # Ordered by tiger_count
-
#
-
# @param [Hash] query
-
# A hash describing the conditions and order for the query
-
# @return [Collection]
-
# A set of records found matching the conditions in +query+
-
# @see Collection
-
#
-
# @api public
-
1
def all(query = Undefined)
-
if query.equal?(Undefined) || (query.kind_of?(Hash) && query.empty?)
-
# TODO: after adding Enumerable methods to Model, try to return self here
-
new_collection(self.query.dup)
-
else
-
new_collection(scoped_query(query))
-
end
-
end
-
-
# Return the first Resource or the first N Resources for the Model with an optional query
-
#
-
# When there are no arguments, return the first Resource in the
-
# Model. When the first argument is an Integer, return a
-
# Collection containing the first N Resources. When the last
-
# (optional) argument is a Hash scope the results to the query.
-
#
-
# @param [Integer] limit (optional)
-
# limit the returned Collection to a specific number of entries
-
# @param [Hash] query (optional)
-
# scope the returned Resource or Collection to the supplied query
-
#
-
# @return [Resource, Collection]
-
# The first resource in the entries of this collection,
-
# or a new collection whose query has been merged
-
#
-
# @api public
-
1
def first(*args)
-
100
first_arg = args.first
-
100
last_arg = args.last
-
-
100
limit_specified = first_arg.kind_of?(Integer)
-
100
with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
-
-
100
limit = limit_specified ? first_arg : 1
-
100
query = with_query ? last_arg : {}
-
-
100
query = self.query.slice(0, limit).update(query)
-
-
100
if limit_specified
-
all(query)
-
else
-
100
query.repository.read(query).first
-
end
-
end
-
-
# Return the last Resource or the last N Resources for the Model with an optional query
-
#
-
# When there are no arguments, return the last Resource for the
-
# Model. When the first argument is an Integer, return a
-
# Collection containing the last N Resources. When the last
-
# (optional) argument is a Hash scope the results to the query.
-
#
-
# @param [Integer] limit (optional)
-
# limit the returned Collection to a specific number of entries
-
# @param [Hash] query (optional)
-
# scope the returned Resource or Collection to the supplied query
-
#
-
# @return [Resource, Collection]
-
# The last resource in the entries of this collection,
-
# or a new collection whose query has been merged
-
#
-
# @api public
-
1
def last(*args)
-
first_arg = args.first
-
last_arg = args.last
-
-
limit_specified = first_arg.kind_of?(Integer)
-
with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
-
-
limit = limit_specified ? first_arg : 1
-
query = with_query ? last_arg : {}
-
-
query = self.query.slice(0, limit).update(query).reverse!
-
-
if limit_specified
-
all(query)
-
else
-
query.repository.read(query).last
-
end
-
end
-
-
# Finds the first Resource by conditions, or initializes a new
-
# Resource with the attributes if none found
-
#
-
# @param [Hash] conditions
-
# The conditions to be used to search
-
# @param [Hash] attributes
-
# The attributes to be used to create the record of none is found.
-
# @return [Resource]
-
# The instance found by +query+, or created with +attributes+ if none found
-
#
-
# @api public
-
1
def first_or_new(conditions = {}, attributes = {})
-
first(conditions) || new(conditions.merge(attributes))
-
end
-
-
# Finds the first Resource by conditions, or creates a new
-
# Resource with the attributes if none found
-
#
-
# @param [Hash] conditions
-
# The conditions to be used to search
-
# @param [Hash] attributes
-
# The attributes to be used to create the record of none is found.
-
# @return [Resource]
-
# The instance found by +query+, or created with +attributes+ if none found
-
#
-
# @api public
-
1
def first_or_create(conditions = {}, attributes = {})
-
first(conditions) || create(conditions.merge(attributes))
-
end
-
-
# Create a Resource
-
#
-
# @param [Hash(Symbol => Object)] attributes
-
# attributes to set
-
#
-
# @return [Resource]
-
# the newly created Resource instance
-
#
-
# @api public
-
1
def create(attributes = {})
-
_create(attributes)
-
end
-
-
# Create a Resource, bypassing hooks
-
#
-
# @param [Hash(Symbol => Object)] attributes
-
# attributes to set
-
#
-
# @return [Resource]
-
# the newly created Resource instance
-
#
-
# @api public
-
1
def create!(attributes = {})
-
_create(attributes, false)
-
end
-
-
# Update every Resource
-
#
-
# Person.update(:allow_beer => true)
-
#
-
# @param [Hash] attributes
-
# attributes to update with
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update(attributes)
-
all.update(attributes)
-
end
-
-
# Update every Resource, bypassing validations
-
#
-
# Person.update!(:allow_beer => true)
-
#
-
# @param [Hash] attributes
-
# attributes to update with
-
#
-
# @return [Boolean]
-
# true if the resources were successfully updated
-
#
-
# @api public
-
1
def update!(attributes)
-
all.update!(attributes)
-
end
-
-
# Remove all Resources from the repository
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy
-
all.destroy
-
end
-
-
# Remove all Resources from the repository, bypassing validation
-
#
-
# @return [Boolean]
-
# true if the resources were successfully destroyed
-
#
-
# @api public
-
1
def destroy!
-
all.destroy!
-
end
-
-
# Copy a set of records from one repository to another.
-
#
-
# @param [String] source_repository_name
-
# The name of the Repository the resources should be copied _from_
-
# @param [String] target_repository_name
-
# The name of the Repository the resources should be copied _to_
-
# @param [Hash] query
-
# The conditions with which to find the records to copy. These
-
# conditions are merged with Model.query
-
#
-
# @return [Collection]
-
# A Collection of the Resource instances created in the operation
-
#
-
# @api public
-
1
def copy(source_repository_name, target_repository_name, query = {})
-
target_properties = properties(target_repository_name)
-
-
query[:fields] ||= properties(source_repository_name).select do |property|
-
target_properties.include?(property)
-
end
-
-
repository(target_repository_name) do |repository|
-
resources = []
-
-
all(query.merge(:repository => source_repository_name)).each do |resource|
-
new_resource = new
-
query[:fields].each { |property| new_resource.__send__("#{property.name}=", property.get(resource)) }
-
resources << new_resource if new_resource.save
-
end
-
-
all(Query.target_query(repository, self, resources))
-
end
-
end
-
-
# Loads an instance of this Model, taking into account IdentityMap lookup,
-
# inheritance columns(s) and Property typecasting.
-
#
-
# @param [Enumerable(Object)] records
-
# an Array of Resource or Hashes to load a Resource with
-
#
-
# @return [Resource]
-
# the loaded Resource instance
-
#
-
# @api semipublic
-
1
def load(records, query)
-
83
repository = query.repository
-
83
repository_name = repository.name
-
83
fields = query.fields
-
83
discriminator = properties(repository_name).discriminator
-
83
no_reload = !query.reload?
-
-
222
field_map = Hash[ fields.map { |property| [ property, property.field ] } ]
-
-
83
records.map do |record|
-
34
identity_map = nil
-
34
key_values = nil
-
34
resource = nil
-
-
34
case record
-
when Hash
-
# remap fields to use the Property object
-
34
record = record.dup
-
124
field_map.each { |property, field| record[property] = record.delete(field) if record.key?(field) }
-
-
34
model = discriminator && discriminator.load(record[discriminator]) || self
-
34
model_key = model.key(repository_name)
-
-
34
resource = if model_key.valid?(key_values = record.values_at(*model_key))
-
34
identity_map = repository.identity_map(model)
-
34
identity_map[key_values]
-
end
-
-
34
resource ||= model.allocate
-
-
34
fields.each do |property|
-
90
next if no_reload && property.loaded?(resource)
-
-
90
value = record[property]
-
-
# TODO: typecasting should happen inside the Adapter
-
# and all values should come back as expected objects
-
90
value = property.load(value)
-
-
90
property.set!(resource, value)
-
end
-
-
when Resource
-
model = record.model
-
model_key = model.key(repository_name)
-
-
resource = if model_key.valid?(key_values = record.key)
-
identity_map = repository.identity_map(model)
-
identity_map[key_values]
-
end
-
-
resource ||= model.allocate
-
-
fields.each do |property|
-
next if no_reload && property.loaded?(resource)
-
-
property.set!(resource, property.get!(record))
-
end
-
end
-
-
34
resource.instance_variable_set(:@_repository, repository)
-
-
34
if identity_map
-
34
resource.persistence_state = Resource::PersistenceState::Clean.new(resource) unless resource.persistence_state?
-
-
# defer setting the IdentityMap so second level caches can
-
# record the state of the resource after loaded
-
34
identity_map[key_values] = resource
-
else
-
resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
-
end
-
-
34
resource
-
end
-
end
-
-
# @api semipublic
-
1
attr_reader :base_model
-
-
# The list of writer methods that can be mass-assigned to in #attributes=
-
#
-
# @return [Set]
-
#
-
# @api private
-
1
attr_reader :allowed_writer_methods
-
-
# @api semipublic
-
1
def default_repository_name
-
2985
Repository.default_name
-
end
-
-
# @api semipublic
-
1
def default_order(repository_name = default_repository_name)
-
149
@default_order[repository_name] ||= key(repository_name).map { |property| Query::Direction.new(property) }.freeze
-
end
-
-
# Get the repository with a given name, or the default one for the current
-
# context, or the default one for this class.
-
#
-
# @param [Symbol] name
-
# the name of the repository wanted
-
# @param [Block] block
-
# block to execute with the fetched repository as parameter
-
#
-
# @return [Object, Respository]
-
# whatever the block returns, if given a block,
-
# otherwise the requested repository.
-
#
-
# @api private
-
1
def repository(name = nil, &block)
-
#
-
# There has been a couple of different strategies here, but me (zond) and dkubb are at least
-
# united in the concept of explicitness over implicitness. That is - the explicit wish of the
-
# caller (+name+) should be given more priority than the implicit wish of the caller (Repository.context.last).
-
#
-
-
1057
DataMapper.repository(name || repository_name, &block)
-
end
-
-
# Get the current +repository_name+ for this Model.
-
#
-
# If there are any Repository contexts, the name of the last one will
-
# be returned, else the +default_repository_name+ of this model will be
-
#
-
# @return [String]
-
# the current repository name to use for this Model
-
#
-
# @api private
-
1
def repository_name
-
1141
context = Repository.context
-
1141
context.any? ? context.last.name : default_repository_name
-
end
-
-
# Gets the current Set of repositories for which
-
# this Model has been defined (beyond default)
-
#
-
# @return [Set]
-
# The Set of repositories for which this Model
-
# has been defined (beyond default)
-
#
-
# @api private
-
1
def repositories
-
[ repository ].to_set + @properties.keys.map { |repository_name| DataMapper.repository(repository_name) }
-
end
-
-
1
private
-
-
# @api private
-
1
def _create(attributes, execute_hooks = true)
-
resource = new(attributes)
-
resource.__send__(execute_hooks ? :save : :save!)
-
resource
-
end
-
-
# @api private
-
1
def const_missing(name)
-
2
if name == :DM
-
raise "#{name} prefix deprecated and no longer necessary (#{caller.first})"
-
2
elsif name == :Resource
-
2
Resource
-
else
-
super
-
end
-
end
-
-
# @api private
-
1
def default_storage_name
-
5
base_model.name
-
end
-
-
# Initializes a new Collection
-
#
-
# @return [Collection]
-
# A new Collection object
-
#
-
# @api private
-
1
def new_collection(query, resources = nil, &block)
-
Collection.new(query, resources, &block)
-
end
-
-
# @api private
-
# TODO: move the logic to create relative query into Query
-
1
def scoped_query(query)
-
12
if query.kind_of?(Query)
-
query.dup
-
else
-
12
repository = if query.key?(:repository)
-
query = query.dup
-
repository = query.delete(:repository)
-
-
if repository.kind_of?(Symbol)
-
DataMapper.repository(repository)
-
else
-
repository
-
end
-
else
-
12
self.repository
-
end
-
-
12
query = self.query.merge(query)
-
-
12
if self.query.repository == repository
-
12
query
-
else
-
repository.new_query(self, query.options)
-
end
-
end
-
end
-
-
# Initialize all foreign key properties established by relationships
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def finalize_relationships
-
34
relationships(repository_name).each { |relationship| relationship.finalize }
-
end
-
-
# Initialize the list of allowed writer methods
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def finalize_allowed_writer_methods
-
1334
@allowed_writer_methods = public_instance_methods.map { |method| method.to_s }.grep(WRITER_METHOD_REGEXP).to_set
-
10
@allowed_writer_methods -= INVALID_WRITER_METHODS
-
10
@allowed_writer_methods.freeze
-
end
-
-
# @api private
-
# TODO: Remove this once appropriate warnings can be added.
-
1
def assert_valid(force = false) # :nodoc:
-
5
return if @valid && !force
-
5
@valid = true
-
5
finalize
-
end
-
-
# Raises an exception if #get receives the wrong number of arguments
-
#
-
# @param [Array] key
-
# the key value
-
#
-
# @return [undefined]
-
#
-
# @raise [UpdateConflictError]
-
# raise if the resource is dirty
-
#
-
# @api private
-
1
def assert_valid_key_size(key)
-
29
expected_key_size = self.key(repository_name).size
-
29
actual_key_size = key.size
-
-
29
if actual_key_size != expected_key_size
-
raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
-
end
-
end
-
-
# Test if the model name is valid
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def assert_valid_name
-
10
if name.to_s.strip.empty?
-
raise IncompleteModelError, "#{inspect} must have a name"
-
end
-
end
-
-
# Test if the model has properties
-
#
-
# A model may also be valid if it has at least one m:1 relationships which
-
# will add inferred foreign key properties.
-
#
-
# @return [undefined]
-
#
-
# @raise [IncompleteModelError]
-
# raised if the model has no properties
-
#
-
# @api private
-
1
def assert_valid_properties
-
10
repository_name = self.repository_name
-
if properties(repository_name).empty? &&
-
10
!relationships(repository_name).any? { |relationship| relationship.kind_of?(Associations::ManyToOne::Relationship) }
-
raise IncompleteModelError, "#{name} must have at least one property or many to one relationship to be valid"
-
end
-
end
-
-
# Test if the model has a valid key
-
#
-
# @return [undefined]
-
#
-
# @raise [IncompleteModelError]
-
# raised if the model does not have a valid key
-
#
-
# @api private
-
1
def assert_valid_key
-
10
if key(repository_name).empty?
-
raise IncompleteModelError, "#{name} must have a key to be valid"
-
end
-
end
-
-
end # module Model
-
end # module DataMapper
-
1
module DataMapper
-
1
module Model
-
1
module Hook
-
1
Model.append_inclusions self
-
-
1
extend Chainable
-
-
1
def self.included(model)
-
5
model.send(:include, DataMapper::Hook)
-
5
model.extend Methods
-
end
-
-
1
module Methods
-
1
def inherited(model)
-
copy_hooks(model)
-
super
-
end
-
-
# @api public
-
1
def before(target_method, method_sym = nil, &block)
-
5
setup_hook(:before, target_method, method_sym, block) { super }
-
end
-
-
# @api public
-
1
def after(target_method, method_sym = nil, &block)
-
setup_hook(:after, target_method, method_sym, block) { super }
-
end
-
-
# @api private
-
1
def hooks
-
@hooks ||= {
-
:save => { :before => [], :after => [] },
-
:create => { :before => [], :after => [] },
-
:update => { :before => [], :after => [] },
-
:destroy => { :before => [], :after => [] },
-
85
}
-
end
-
-
1
private
-
-
1
def setup_hook(type, name, method, proc)
-
5
types = hooks[name]
-
5
if types && types[type]
-
5
types[type] << if proc
-
ProcCommand.new(proc)
-
else
-
5
MethodCommand.new(self, method)
-
5
end
-
else
-
yield
-
end
-
end
-
-
# deep copy hooks from the parent model
-
1
def copy_hooks(model)
-
hooks = Hash.new do |hooks, name|
-
hooks[name] = Hash.new do |types, type|
-
if self.hooks[name]
-
types[type] = self.hooks[name][type].map do |command|
-
command.copy(model)
-
end
-
end
-
end
-
end
-
-
model.instance_variable_set(:@hooks, hooks)
-
end
-
-
end
-
-
1
class ProcCommand
-
1
def initialize(proc)
-
@proc = proc.to_proc
-
end
-
-
1
def call(resource)
-
resource.instance_eval(&@proc)
-
end
-
-
1
def copy(model)
-
self
-
end
-
end
-
-
1
class MethodCommand
-
1
def initialize(model, method)
-
5
@model, @method = model, method.to_sym
-
end
-
-
1
def call(resource)
-
20
resource.__send__(@method)
-
end
-
-
1
def copy(model)
-
self.class.new(model, @method)
-
end
-
-
end
-
-
end # module Hook
-
end # module Model
-
end # module DataMapper
-
1
module DataMapper
-
1
module Model
-
# Module that provides a common way for plugin authors
-
# to implement "is ... " traits (object behaviors that can be shared)
-
1
module Is
-
# A common interface to activate plugins for a resource. For instance:
-
#
-
# class Widget
-
# include DataMapper::Resource
-
#
-
# is :list
-
# end
-
#
-
# adds list item behavior to the model. Plugin that wants to conform
-
# to "is API" of DataMapper must supply is_+behavior name+ method,
-
# for example above it would be is_list.
-
#
-
# @api public
-
1
def is(plugin, *args, &block)
-
generator_method = "is_#{plugin}".to_sym
-
-
if respond_to?(generator_method)
-
send(generator_method, *args, &block)
-
else
-
raise PluginNotFoundError, "could not find plugin named #{plugin}"
-
end
-
end
-
end # module Is
-
-
1
include Is
-
end # module Model
-
end # module DataMapper
-
# TODO: update Model#respond_to? to return true if method_method missing
-
# would handle the message
-
-
1
module DataMapper
-
1
module Model
-
1
module Property
-
1
Model.append_extensions self, DataMapper::Property::Lookup
-
-
1
def self.extended(model)
-
5
model.instance_variable_set(:@properties, {})
-
5
model.instance_variable_set(:@field_naming_conventions, {})
-
end
-
-
-
1
def inherited(model)
-
model.instance_variable_set(:@properties, {})
-
model.instance_variable_set(:@field_naming_conventions, @field_naming_conventions.dup)
-
-
@properties.each do |repository_name, properties|
-
model_properties = model.properties(repository_name)
-
properties.each { |property| model_properties << property }
-
end
-
-
super
-
end
-
-
# Defines a Property on the Resource
-
#
-
# @param [Symbol] name
-
# the name for which to call this property
-
# @param [Class] type
-
# the ruby type to define this property as
-
# @param [Hash(Symbol => String)] options
-
# a hash of available options
-
#
-
# @return [Property]
-
# the created Property
-
#
-
# @see Property
-
#
-
# @api public
-
1
def property(name, type, options = {})
-
20
if TrueClass == type
-
raise "#{type} is deprecated, use Boolean instead at #{caller[2]}"
-
20
elsif BigDecimal == type
-
raise "#{type} is deprecated, use Decimal instead at #{caller[2]}"
-
end
-
-
# if the type can be found within Property then
-
# use that class rather than the primitive
-
20
unless klass = DataMapper::Property.determine_class(type)
-
raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type"
-
end
-
-
20
property = klass.new(self, name, options)
-
-
20
repository_name = self.repository_name
-
20
properties = properties(repository_name)
-
-
20
properties << property
-
-
# Add property to the other mappings as well if this is for the default
-
# repository.
-
-
20
if repository_name == default_repository_name
-
20
other_repository_properties = DataMapper::Ext::Hash.except(@properties, default_repository_name)
-
-
20
other_repository_properties.each do |other_repository_name, properties|
-
next if properties.named?(name)
-
-
# make sure the property is created within the correct repository scope
-
DataMapper.repository(other_repository_name) do
-
properties << klass.new(self, name, options)
-
end
-
end
-
end
-
-
# Add the property to the lazy_loads set for this resources repository
-
# only.
-
# TODO Is this right or should we add the lazy contexts to all
-
# repositories?
-
20
if property.lazy?
-
2
context = options.fetch(:lazy, :default)
-
2
context = :default if context == true
-
-
2
Array(context).each do |context|
-
2
properties.lazy_context(context) << property
-
end
-
end
-
-
# add the property to the child classes only if the property was
-
# added after the child classes' properties have been copied from
-
# the parent
-
20
descendants.each do |descendant|
-
descendant.properties(repository_name) << property
-
end
-
-
20
create_reader_for(property)
-
20
create_writer_for(property)
-
-
# FIXME: explicit return needed for YARD to parse this properly
-
20
return property
-
end
-
-
# Gets a list of all properties that have been defined on this Model in
-
# the requested repository
-
#
-
# @param [Symbol, String] repository_name
-
# The name of the repository to use. Uses the default Repository
-
# if none is specified.
-
#
-
# @return [PropertySet]
-
# A list of Properties defined on this Model in the given Repository
-
#
-
# @api public
-
1
def properties(repository_name = default_repository_name)
-
# TODO: create PropertySet#copy that will copy the properties, but assign the
-
# new Relationship objects to a supplied repository and model. dup does not really
-
# do what is needed
-
1388
repository_name = repository_name.to_sym
-
-
1388
default_repository_name = self.default_repository_name
-
-
1388
@properties[repository_name] ||= if repository_name == default_repository_name
-
5
PropertySet.new
-
else
-
properties(default_repository_name).dup
-
end
-
end
-
-
# Gets the list of key fields for this Model in +repository_name+
-
#
-
# @param [String] repository_name
-
# The name of the Repository for which the key is to be reported
-
#
-
# @return [Array]
-
# The list of key fields for this Model in +repository_name+
-
#
-
# @api public
-
1
def key(repository_name = default_repository_name)
-
282
properties(repository_name).key
-
end
-
-
# @api public
-
1
def serial(repository_name = default_repository_name)
-
40
key(repository_name).detect { |property| property.serial? }
-
end
-
-
# Gets the field naming conventions for this resource in the given Repository
-
#
-
# @param [String, Symbol] repository_name
-
# the name of the Repository for which the field naming convention
-
# will be retrieved
-
#
-
# @return [#call]
-
# The naming convention for the given Repository
-
#
-
# @api public
-
1
def field_naming_convention(repository_name = default_storage_name)
-
20
@field_naming_conventions[repository_name] ||= repository(repository_name).adapter.field_naming_convention
-
end
-
-
# @api private
-
1
def properties_with_subclasses(repository_name = default_repository_name)
-
10
properties = properties(repository_name).dup
-
-
10
descendants.each do |model|
-
model.properties(repository_name).each do |property|
-
properties << property
-
end
-
end
-
-
10
properties
-
end
-
-
# @api private
-
1
def key_conditions(repository, key)
-
31
Hash[ self.key(repository.name).zip(key.nil? ? [] : key) ]
-
end
-
-
1
private
-
-
# Defines the anonymous module that is used to add properties.
-
# Using a single module here prevents having a very large number
-
# of anonymous modules, where each property has their own module.
-
# @api private
-
1
def property_module
-
@property_module ||= begin
-
5
mod = Module.new
-
5
class_eval do
-
5
include mod
-
end
-
5
mod
-
41
end
-
end
-
-
# defines the reader method for the property
-
#
-
# @api private
-
1
def create_reader_for(property)
-
20
name = property.name.to_s
-
20
reader_visibility = property.reader_visibility
-
20
instance_variable_name = property.instance_variable_name
-
20
property_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{reader_visibility}
-
def #{name}
-
return #{instance_variable_name} if defined?(#{instance_variable_name})
-
property = properties[#{name.inspect}]
-
#{instance_variable_name} = property ? persistence_state.get(property) : nil
-
end
-
RUBY
-
-
20
boolean_reader_name = "#{name}?"
-
-
20
if property.kind_of?(DataMapper::Property::Boolean)
-
1
property_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{reader_visibility}
-
def #{boolean_reader_name}
-
#{name}
-
end
-
RUBY
-
end
-
end
-
-
# defines the setter for the property
-
#
-
# @api private
-
1
def create_writer_for(property)
-
20
name = property.name
-
20
writer_visibility = property.writer_visibility
-
-
20
writer_name = "#{name}="
-
20
property_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{writer_visibility}
-
def #{writer_name}(value)
-
property = properties[#{name.inspect}]
-
value = property.typecast(value)
-
self.persistence_state = persistence_state.set(property, value)
-
persistence_state.get(property)
-
end
-
RUBY
-
end
-
-
# @api public
-
1
def method_missing(method, *args, &block)
-
if property = properties(repository_name)[method]
-
return property
-
end
-
-
super
-
end
-
end # module Property
-
end # module Model
-
end # module DataMapper
-
# TODO: update Model#respond_to? to return true if method_method missing
-
# would handle the message
-
-
1
module DataMapper
-
1
module Model
-
1
module Relationship
-
1
Model.append_extensions self
-
-
1
include DataMapper::Assertions
-
-
# Initializes relationships hash for extended model
-
# class.
-
#
-
# When model calls has n, has 1 or belongs_to, relationships
-
# are stored in that hash: keys are repository names and
-
# values are relationship sets.
-
#
-
# @api private
-
1
def self.extended(model)
-
5
model.instance_variable_set(:@relationships, {})
-
end
-
-
# When DataMapper model is inherited, relationships
-
# of parent are duplicated and copied to subclass model
-
#
-
# @api private
-
1
def inherited(model)
-
model.instance_variable_set(:@relationships, {})
-
-
@relationships.each do |repository_name, relationships|
-
model_relationships = model.relationships(repository_name)
-
relationships.each { |relationship| model_relationships << relationship }
-
end
-
-
super
-
end
-
-
# Returns copy of relationships set in given repository.
-
#
-
# @param [Symbol] repository_name
-
# Name of the repository for which relationships set is returned
-
# @return [RelationshipSet] relationships set for given repository
-
#
-
# @api semipublic
-
1
def relationships(repository_name = default_repository_name)
-
# TODO: create RelationshipSet#copy that will copy the relationships, but assign the
-
# new Relationship objects to a supplied repository and model. dup does not really
-
# do what is needed
-
-
300
default_repository_name = self.default_repository_name
-
-
300
@relationships[repository_name] ||= if repository_name == default_repository_name
-
5
RelationshipSet.new
-
else
-
relationships(default_repository_name).dup
-
end
-
end
-
-
# Used to express unlimited cardinality of association,
-
# see +has+
-
#
-
# @api public
-
1
def n
-
10
Infinity
-
end
-
-
# A shorthand, clear syntax for defining one-to-one, one-to-many and
-
# many-to-many resource relationships.
-
#
-
# * has 1, :friend # one friend
-
# * has n, :friends # many friends
-
# * has 1..3, :friends # many friends (at least 1, at most 3)
-
# * has 3, :friends # many friends (exactly 3)
-
# * has 1, :friend, 'User' # one friend with the class User
-
# * has 3, :friends, :through => :friendships # many friends through the friendships relationship
-
#
-
# @param cardinality [Integer, Range, Infinity]
-
# cardinality that defines the association type and constraints
-
# @param name [Symbol]
-
# the name that the association will be referenced by
-
# @param *args [Model, Hash] model and/or options hash
-
#
-
# @option *args :through[Symbol] A association that this join should go through to form
-
# a many-to-many association
-
# @option *args :model[Model, String] The name of the class to associate with, if omitted
-
# then the association name is assumed to match the class name
-
# @option *args :repository[Symbol] name of child model repository
-
#
-
# @return [Association::Relationship] the relationship that was
-
# created to reflect either a one-to-one, one-to-many or many-to-many
-
# relationship
-
# @raise [ArgumentError] if the cardinality was not understood. Should be a
-
# Integer, Range or Infinity(n)
-
#
-
# @api public
-
1
def has(cardinality, name, *args)
-
6
name = name.to_sym
-
6
model = extract_model(args)
-
6
options = extract_options(args)
-
-
6
min, max = extract_min_max(cardinality)
-
6
options.update(:min => min, :max => max)
-
-
6
assert_valid_options(options)
-
-
6
if options.key?(:model) && model
-
raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument'
-
end
-
-
6
model ||= options.delete(:model)
-
-
6
repository_name = repository.name
-
-
# TODO: change to :target_respository_name and :source_repository_name
-
6
options[:child_repository_name] = options.delete(:repository)
-
6
options[:parent_repository_name] = repository_name
-
-
6
klass = if max > 1
-
6
options.key?(:through) ? Associations::ManyToMany::Relationship : Associations::OneToMany::Relationship
-
else
-
Associations::OneToOne::Relationship
-
end
-
-
6
relationship = klass.new(name, model, self, options)
-
-
6
relationships(repository_name) << relationship
-
-
6
descendants.each do |descendant|
-
descendant.relationships(repository_name) << relationship
-
end
-
-
6
create_relationship_reader(relationship)
-
6
create_relationship_writer(relationship)
-
-
6
relationship
-
end
-
-
# A shorthand, clear syntax for defining many-to-one resource relationships.
-
#
-
# * belongs_to :user # many to one user
-
# * belongs_to :friend, :model => 'User' # many to one friend
-
# * belongs_to :reference, :repository => :pubmed # association for repository other than default
-
#
-
# @param name [Symbol]
-
# the name that the association will be referenced by
-
# @param *args [Model, Hash] model and/or options hash
-
#
-
# @option *args :model[Model, String] The name of the class to associate with, if omitted
-
# then the association name is assumed to match the class name
-
# @option *args :repository[Symbol] name of child model repository
-
#
-
# @return [Association::Relationship] The association created
-
# should not be accessed directly
-
#
-
# @api public
-
1
def belongs_to(name, *args)
-
6
name = name.to_sym
-
6
model_name = self.name
-
6
model = extract_model(args)
-
6
options = extract_options(args)
-
-
6
if options.key?(:through)
-
raise "#{model_name}#belongs_to with :through is deprecated, use 'has 1, :#{name}, #{options.inspect}' in #{model_name} instead (#{caller.first})"
-
6
elsif options.key?(:model) && model
-
raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument'
-
end
-
-
6
assert_valid_options(options)
-
-
6
model ||= options.delete(:model)
-
-
6
repository_name = repository.name
-
-
# TODO: change to source_repository_name and target_respository_name
-
6
options[:child_repository_name] = repository_name
-
6
options[:parent_repository_name] = options.delete(:repository)
-
-
6
relationship = Associations::ManyToOne::Relationship.new(name, self, model, options)
-
-
6
relationships(repository_name) << relationship
-
-
6
descendants.each do |descendant|
-
descendant.relationships(repository_name) << relationship
-
end
-
-
6
create_relationship_reader(relationship)
-
6
create_relationship_writer(relationship)
-
-
6
relationship
-
end
-
-
1
private
-
-
# Extract the model from an Array of arguments
-
#
-
# @param [Array(Model, String, Hash)]
-
# The arguments passed to an relationship declaration
-
#
-
# @return [Model, #to_str]
-
# target model for the association
-
#
-
# @api private
-
1
def extract_model(args)
-
12
model = args.first
-
-
12
if model.kind_of?(Model)
-
4
model
-
8
elsif model.respond_to?(:to_str)
-
model.to_str
-
else
-
nil
-
end
-
end
-
-
# Extract the model from an Array of arguments
-
#
-
# @param [Array(Model, String, Hash)]
-
# The arguments passed to an relationship declaration
-
#
-
# @return [Hash]
-
# options for the association
-
#
-
# @api private
-
1
def extract_options(args)
-
12
options = args.last
-
12
options.respond_to?(:to_hash) ? options.to_hash.dup : {}
-
end
-
-
# A support method for converting Integer, Range or Infinity values into two
-
# values representing the minimum and maximum cardinality of the association
-
#
-
# @return [Array] A pair of integers, min and max
-
#
-
# @api private
-
1
def extract_min_max(cardinality)
-
6
case cardinality
-
when Integer then [ cardinality, cardinality ]
-
2
when Range then [ cardinality.first, cardinality.last ]
-
4
when Infinity then [ 0, Infinity ]
-
else
-
assert_kind_of 'options', options, Integer, Range, Infinity.class
-
end
-
end
-
-
# Validates options of association method like belongs_to or has:
-
# verifies types of cardinality bounds, repository, association class,
-
# keys and possible values of :through option.
-
#
-
# @api private
-
1
def assert_valid_options(options)
-
# TODO: update to match Query#assert_valid_options
-
# - perform options normalization elsewhere
-
-
12
if options.key?(:min) && options.key?(:max)
-
6
min = options[:min]
-
6
max = options[:max]
-
-
6
min = min.to_int unless min == Infinity
-
6
max = max.to_int unless max == Infinity
-
-
6
if min == Infinity && max == Infinity
-
raise ArgumentError, 'Cardinality may not be n..n. The cardinality specifies the min/max number of results from the association'
-
6
elsif min > max
-
raise ArgumentError, "Cardinality min (#{min}) cannot be larger than the max (#{max})"
-
6
elsif min < 0
-
raise ArgumentError, "Cardinality min much be greater than or equal to 0, but was #{min}"
-
6
elsif max < 1
-
raise ArgumentError, "Cardinality max much be greater than or equal to 1, but was #{max}"
-
end
-
end
-
-
12
if options.key?(:repository)
-
options[:repository] = options[:repository].to_sym
-
end
-
-
12
if options.key?(:class_name)
-
raise "+options[:class_name]+ is deprecated, use :model instead (#{caller[1]})"
-
12
elsif options.key?(:remote_name)
-
raise "+options[:remote_name]+ is deprecated, use :via instead (#{caller[1]})"
-
end
-
-
12
if options.key?(:through)
-
2
assert_kind_of 'options[:through]', options[:through], Symbol, Module
-
end
-
-
12
[ :via, :inverse ].each do |key|
-
24
if options.key?(key)
-
assert_kind_of "options[#{key.inspect}]", options[key], Symbol, Associations::Relationship
-
end
-
end
-
-
# TODO: deprecate :child_key and :parent_key in favor of :source_key and
-
# :target_key (will mean something different for each relationship)
-
-
12
[ :child_key, :parent_key ].each do |key|
-
24
if options.key?(key)
-
4
options[key] = Array(options[key])
-
end
-
end
-
-
12
if options.key?(:limit)
-
raise ArgumentError, '+options[:limit]+ should not be specified on a relationship'
-
end
-
end
-
-
# Defines the anonymous module that is used to add relationships.
-
# Using a single module here prevents having a very large number
-
# of anonymous modules, where each property has their own module.
-
# @api private
-
1
def relationship_module
-
@relationship_module ||= begin
-
5
mod = Module.new
-
5
class_eval do
-
5
include mod
-
end
-
5
mod
-
24
end
-
end
-
-
# Dynamically defines reader method
-
#
-
# @api private
-
1
def create_relationship_reader(relationship)
-
12
name = relationship.name
-
12
reader_name = name.to_s
-
-
12
return if method_defined?(reader_name)
-
-
12
reader_visibility = relationship.reader_visibility
-
-
12
relationship_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{reader_visibility}
-
def #{reader_name}(query = nil)
-
# TODO: when no query is passed in, return the results from
-
# the ivar directly. This will require that the ivar
-
# actually hold the resource/collection, and in the case
-
# of 1:1, the underlying collection is hidden in a
-
# private ivar, and the resource is in a known ivar
-
-
persistence_state.get(relationships[#{name.inspect}], query)
-
end
-
RUBY
-
end
-
-
# Dynamically defines writer method
-
#
-
# @api private
-
1
def create_relationship_writer(relationship)
-
12
name = relationship.name
-
12
writer_name = "#{name}="
-
-
12
return if method_defined?(writer_name)
-
-
12
writer_visibility = relationship.writer_visibility
-
-
12
relationship_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{writer_visibility}
-
def #{writer_name}(target)
-
relationship = relationships[#{name.inspect}]
-
self.persistence_state = persistence_state.set(relationship, target)
-
persistence_state.get(relationship)
-
end
-
RUBY
-
end
-
-
# @api public
-
1
def method_missing(method, *args, &block)
-
if relationship = relationships(repository_name)[method]
-
return Query::Path.new([ relationship ])
-
end
-
-
super
-
end
-
-
end # module Relationship
-
end # module Model
-
end # module DataMapper
-
1
module DataMapper
-
1
module Model
-
# Module with query scoping functionality.
-
#
-
# Scopes are implemented using simple array based
-
# stack that is thread local. Default scope can be set
-
# on a per repository basis.
-
#
-
# Scopes are merged as new queries are nested.
-
# It is also possible to get exclusive scope access
-
# using +with_exclusive_scope+
-
1
module Scope
-
# @api private
-
1
def default_scope(repository_name = default_repository_name)
-
144
@default_scope ||= {}
-
-
144
default_repository_name = self.default_repository_name
-
-
144
@default_scope[repository_name] ||= if repository_name == default_repository_name
-
3
{}
-
else
-
default_scope(default_repository_name).dup
-
end
-
end
-
-
# Returns query on top of scope stack
-
#
-
# @api private
-
1
def query
-
144
repository.new_query(self, current_scope).freeze
-
end
-
-
# @api private
-
1
def current_scope
-
144
scope_stack.last || default_scope(repository.name)
-
end
-
-
1
protected
-
-
# Pushes given query on top of the stack
-
#
-
# @param [Hash, Query] Query to add to current scope nesting
-
#
-
# @api private
-
1
def with_scope(query)
-
options = if query.kind_of?(Hash)
-
query
-
else
-
query.options
-
end
-
-
# merge the current scope with the passed in query
-
with_exclusive_scope(self.query.merge(options)) { |*block_args| yield(*block_args) }
-
end
-
-
# Pushes given query on top of scope stack and yields
-
# given block, then pops the stack. During block execution
-
# queries previously pushed onto the stack
-
# have no effect.
-
#
-
# @api private
-
1
def with_exclusive_scope(query)
-
query = if query.kind_of?(Hash)
-
repository.new_query(self, query)
-
else
-
query.dup
-
end
-
-
scope_stack = self.scope_stack
-
scope_stack << query.options
-
-
begin
-
yield query.freeze
-
ensure
-
scope_stack.pop
-
end
-
end
-
-
# Initializes (if necessary) and returns current scope stack
-
# @api private
-
1
def scope_stack
-
144
scope_stack_for = Thread.current[:dm_scope_stack] ||= {}
-
144
scope_stack_for[object_id] ||= []
-
end
-
end # module Scope
-
-
1
include Scope
-
end # module Model
-
end # module DataMapper
-
1
module DataMapper
-
# = Properties
-
# Properties for a model are not derived from a database structure, but
-
# instead explicitly declared inside your model class definitions. These
-
# properties then map (or, if using automigrate, generate) fields in your
-
# repository/database.
-
#
-
# If you are coming to DataMapper from another ORM framework, such as
-
# ActiveRecord, this may be a fundamental difference in thinking to you.
-
# However, there are several advantages to defining your properties in your
-
# models:
-
#
-
# * information about your model is centralized in one place: rather than
-
# having to dig out migrations, xml or other configuration files.
-
# * use of mixins can be applied to model properties: better code reuse
-
# * having information centralized in your models, encourages you and the
-
# developers on your team to take a model-centric view of development.
-
# * it provides the ability to use Ruby's access control functions.
-
# * and, because DataMapper only cares about properties explicitly defined
-
# in your models, DataMapper plays well with legacy databases, and shares
-
# databases easily with other applications.
-
#
-
# == Declaring Properties
-
# Inside your class, you call the property method for each property you want
-
# to add. The only two required arguments are the name and type, everything
-
# else is optional.
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String, :required => true # Cannot be null
-
# property :publish, Boolean, :default => false # Default value for new records is false
-
# end
-
#
-
# By default, DataMapper supports the following primitive (Ruby) types
-
# also called core properties:
-
#
-
# * Boolean
-
# * Class (datastore primitive is the same as String. Used for Inheritance)
-
# * Date
-
# * DateTime
-
# * Decimal
-
# * Float
-
# * Integer
-
# * Object (marshalled out during serialization)
-
# * String (default length is 50)
-
# * Text (limit of 65k characters by default)
-
# * Time
-
#
-
# == Limiting Access
-
# Property access control is uses the same terminology Ruby does. Properties
-
# are public by default, but can also be declared private or protected as
-
# needed (via the :accessor option).
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String, :accessor => :private # Both reader and writer are private
-
# property :body, Text, :accessor => :protected # Both reader and writer are protected
-
# end
-
#
-
# Access control is also analogous to Ruby attribute readers and writers, and can
-
# be declared using :reader and :writer, in addition to :accessor.
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String, :writer => :private # Only writer is private
-
# property :tags, String, :reader => :protected # Only reader is protected
-
# end
-
#
-
# == Overriding Accessors
-
# The reader/writer for any property can be overridden in the same manner that Ruby
-
# attr readers/writers can be. After the property is defined, just add your custom
-
# reader or writer:
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String
-
#
-
# def title=(new_title)
-
# raise ArgumentError if new_title != 'Lee is l337'
-
# super(new_title)
-
# end
-
# end
-
#
-
# Calling super ensures that any validators defined for the property are kept active.
-
#
-
# == Lazy Loading
-
# By default, some properties are not loaded when an object is fetched in
-
# DataMapper. These lazily loaded properties are fetched on demand when their
-
# accessor is called for the first time (as it is often unnecessary to
-
# instantiate -every- property -every- time an object is loaded). For
-
# instance, DataMapper::Property::Text fields are lazy loading by default,
-
# although you can over-ride this behavior if you wish:
-
#
-
# Example:
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String # Loads normally
-
# property :body, Text # Is lazily loaded by default
-
# end
-
#
-
# If you want to over-ride the lazy loading on any field you can set it to a
-
# context or false to disable it with the :lazy option. Contexts allow
-
# multiple lazy properties to be loaded at one time. If you set :lazy to
-
# true, it is placed in the :default context
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String # Loads normally
-
# property :body, Text, :lazy => false # The default is now over-ridden
-
# property :comment, String, :lazy => [ :detailed ] # Loads in the :detailed context
-
# property :author, String, :lazy => [ :summary, :detailed ] # Loads in :summary & :detailed context
-
# end
-
#
-
# Delaying the request for lazy-loaded attributes even applies to objects
-
# accessed through associations. In a sense, DataMapper anticipates that
-
# you will likely be iterating over objects in associations and rolls all
-
# of the load commands for lazy-loaded properties into one request from
-
# the database.
-
#
-
# Example:
-
#
-
# Widget.get(1).components
-
# # loads when the post object is pulled from database, by default
-
#
-
# Widget.get(1).components.first.body
-
# # loads the values for the body property on all objects in the
-
# # association, rather than just this one.
-
#
-
# Widget.get(1).components.first.comment
-
# # loads both comment and author for all objects in the association
-
# # since they are both in the :detailed context
-
#
-
# == Keys
-
# Properties can be declared as primary or natural keys on a table.
-
# You should a property as the primary key of the table:
-
#
-
# Examples:
-
#
-
# property :id, Serial # auto-incrementing key
-
# property :legacy_pk, String, :key => true # 'natural' key
-
#
-
# This is roughly equivalent to ActiveRecord's <tt>set_primary_key</tt>,
-
# though non-integer data types may be used, thus DataMapper supports natural
-
# keys. When a property is declared as a natural key, accessing the object
-
# using the indexer syntax <tt>Class[key]</tt> remains valid.
-
#
-
# User.get(1)
-
# # when :id is the primary key on the users table
-
# User.get('bill')
-
# # when :name is the primary (natural) key on the users table
-
#
-
# == Indices
-
# You can add indices for your properties by using the <tt>:index</tt>
-
# option. If you use <tt>true</tt> as the option value, the index will be
-
# automatically named. If you want to name the index yourself, use a symbol
-
# as the value.
-
#
-
# property :last_name, String, :index => true
-
# property :first_name, String, :index => :name
-
#
-
# You can create multi-column composite indices by using the same symbol in
-
# all the columns belonging to the index. The columns will appear in the
-
# index in the order they are declared.
-
#
-
# property :last_name, String, :index => :name
-
# property :first_name, String, :index => :name
-
# # => index on (last_name, first_name)
-
#
-
# If you want to make the indices unique, use <tt>:unique_index</tt> instead
-
# of <tt>:index</tt>
-
#
-
# == Inferred Validations
-
# If you require the dm-validations plugin, auto-validations will
-
# automatically be mixed-in in to your model classes: validation rules that
-
# are inferred when properties are declared with specific column restrictions.
-
#
-
# class Post
-
# include DataMapper::Resource
-
#
-
# property :title, String, :length => 250, :min => 0, :max => 250
-
# # => infers 'validates_length :title'
-
#
-
# property :title, String, :required => true
-
# # => infers 'validates_present :title'
-
#
-
# property :email, String, :format => :email_address
-
# # => infers 'validates_format :email, :with => :email_address'
-
#
-
# property :title, String, :length => 255, :required => true
-
# # => infers both 'validates_length' as well as 'validates_present'
-
# # better: property :title, String, :length => 1..255
-
# end
-
#
-
# This functionality is available with the dm-validations gem. For more information
-
# about validations, check the documentation for dm-validations.
-
#
-
# == Default Values
-
# To set a default for a property, use the <tt>:default</tt> key. The
-
# property will be set to the value associated with that key the first time
-
# it is accessed, or when the resource is saved if it hasn't been set with
-
# another value already. This value can be a static value, such as 'hello'
-
# but it can also be a proc that will be evaluated when the property is read
-
# before its value has been set. The property is set to the return of the
-
# proc. The proc is passed two values, the resource the property is being set
-
# for and the property itself.
-
#
-
# property :display_name, String, :default => lambda { |resource, property| resource.login }
-
#
-
# Word of warning. Don't try to read the value of the property you're setting
-
# the default for in the proc. An infinite loop will ensue.
-
#
-
# == Embedded Values (not implemented yet)
-
# As an alternative to extraneous has_one relationships, consider using an
-
# EmbeddedValue.
-
#
-
# == Property options reference
-
#
-
# :accessor if false, neither reader nor writer methods are
-
# created for this property
-
#
-
# :reader if false, reader method is not created for this property
-
#
-
# :writer if false, writer method is not created for this property
-
#
-
# :lazy if true, property value is only loaded when on first read
-
# if false, property value is always loaded
-
# if a symbol, property value is loaded with other properties
-
# in the same group
-
#
-
# :default default value of this property
-
#
-
# :allow_nil if true, property may have a nil value on save
-
#
-
# :key name of the key associated with this property.
-
#
-
# :field field in the data-store which the property corresponds to
-
#
-
# :length string field length
-
#
-
# :format format for autovalidation. Use with dm-validations plugin.
-
#
-
# :index if true, index is created for the property. If a Symbol, index
-
# is named after Symbol value instead of being based on property name.
-
#
-
# :unique_index true specifies that index on this property should be unique
-
#
-
# :auto_validation if true, automatic validation is performed on the property
-
#
-
# :validates validation context. Use together with dm-validations.
-
#
-
# :unique if true, property column is unique. Properties of type Serial
-
# are unique by default.
-
#
-
# :precision Indicates the number of significant digits. Usually only makes sense
-
# for float type properties. Must be >= scale option value. Default is 10.
-
#
-
# :scale The number of significant digits to the right of the decimal point.
-
# Only makes sense for float type properties. Must be > 0.
-
# Default is nil for Float type and 10 for BigDecimal
-
#
-
# == Overriding default Property options
-
#
-
# There is the ability to reconfigure a Property and it's subclasses by explicitly
-
# setting a value in the Property, eg:
-
#
-
# # set all String properties to have a default length of 255
-
# DataMapper::Property::String.length(255)
-
#
-
# # set all Boolean properties to not allow nil (force true or false)
-
# DataMapper::Property::Boolean.allow_nil(false)
-
#
-
# # set all properties to be required by default
-
# DataMapper::Property.required(true)
-
#
-
# # turn off auto-validation for all properties by default
-
# DataMapper::Property.auto_validation(false)
-
#
-
# # set all mutator methods to be private by default
-
# DataMapper::Property.writer(:private)
-
#
-
# Please note that this has no effect when a subclass has explicitly
-
# defined it's own option. For example, setting the String length to
-
# 255 will not affect the Text property even though it inherits from
-
# String, because it sets it's own default length to 65535.
-
#
-
# == Misc. Notes
-
# * Properties declared as strings will default to a length of 50, rather than
-
# 255 (typical max varchar column size). To overload the default, pass
-
# <tt>:length => 255</tt> or <tt>:length => 0..255</tt>. Since DataMapper
-
# does not introspect for properties, this means that legacy database tables
-
# may need their <tt>String</tt> columns defined with a <tt>:length</tt> so
-
# that DM does not apply an un-needed length validation, or allow overflow.
-
# * You may declare a Property with the data-type of <tt>Class</tt>.
-
# see SingleTableInheritance for more on how to use <tt>Class</tt> columns.
-
1
class Property
-
1
module PassThroughLoadDump
-
# @api semipublic
-
1
def load(value)
-
90
typecast(value) unless value.nil?
-
end
-
-
# Stub instance method for dumping
-
#
-
# @param value [Object, nil] value to dump
-
#
-
# @return [Object] Dumped object
-
#
-
# @api semipublic
-
1
def dump(value)
-
377
value
-
end
-
end
-
-
1
include DataMapper::Assertions
-
1
include Subject
-
1
extend Equalizer
-
-
1
equalize :model, :name, :options
-
-
1
PRIMITIVES = [
-
TrueClass,
-
::String,
-
::Float,
-
::Integer,
-
::BigDecimal,
-
::DateTime,
-
::Date,
-
::Time,
-
::Class
-
].to_set.freeze
-
-
1
OPTIONS = [
-
:accessor, :reader, :writer,
-
:lazy, :default, :key, :field,
-
:index, :unique_index,
-
:unique, :allow_nil, :allow_blank, :required
-
]
-
-
# Possible :visibility option values
-
1
VISIBILITY_OPTIONS = [ :public, :protected, :private ].to_set.freeze
-
-
# Invalid property names
-
1
INVALID_NAMES = (Resource.instance_methods +
-
Resource.private_instance_methods +
-
1
Query::OPTIONS.to_a
-
95
).map { |name| name.to_s }
-
-
1
attr_reader :primitive, :model, :name, :instance_variable_name,
-
:reader_visibility, :writer_visibility, :options,
-
:default, :repository_name, :allow_nil, :allow_blank, :required
-
-
1
class << self
-
1
extend Deprecate
-
-
1
deprecate :all_descendants, :descendants
-
-
# @api semipublic
-
1
def determine_class(type)
-
20
return type if type < DataMapper::Property::Object
-
7
find_class(DataMapper::Inflector.demodulize(type.name))
-
end
-
-
# @api private
-
1
def demodulized_names
-
31
@demodulized_names ||= {}
-
end
-
-
# @api semipublic
-
1
def find_class(name)
-
16
klass = demodulized_names[name]
-
16
klass ||= const_get(name) if const_defined?(name)
-
16
klass
-
end
-
-
# @api public
-
1
def descendants
-
72
@descendants ||= DescendantSet.new
-
end
-
-
# @api private
-
1
def inherited(descendant)
-
# Descendants is a tree rooted in DataMapper::Property that tracks
-
# inheritance. We pre-calculate each comparison value (demodulized
-
# class name) to achieve a Hash[]-time lookup, rather than walk the
-
# entire descendant tree and calculate names on-demand (expensive,
-
# redundant).
-
#
-
# Since the algorithm relegates property class name lookups to a flat
-
# namespace, we need to ensure properties defined outside of DM don't
-
# override built-ins (Serial, String, etc) by merely defining a property
-
# of a same name. We avoid this by only ever adding to the lookup
-
# table. Given that DM loads its own property classes first, we can
-
# assume that their names are "reserved" when added to the table.
-
#
-
# External property authors who want to provide "replacements" for
-
# builtins (e.g. in a non-DM-supported adapter) should follow the
-
# convention of wrapping those properties in a module, and include'ing
-
# the module on the model class directly. This bypasses the DM-hooked
-
# const_missing lookup that would normally check this table.
-
15
descendants << descendant
-
-
15
Property.demodulized_names[DataMapper::Inflector.demodulize(descendant.name)] ||= descendant
-
-
# inherit accepted options
-
15
descendant.accepted_options.concat(accepted_options)
-
-
# inherit the option values
-
31
options.each { |key, value| descendant.send(key, value) }
-
end
-
-
# @api public
-
1
def accepted_options
-
105
@accepted_options ||= []
-
end
-
-
# @api public
-
1
def accept_options(*args)
-
5
accepted_options.concat(args)
-
-
# create methods for each new option
-
5
args.each do |property_option|
-
26
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def self.#{property_option}(value = Undefined) # def self.unique(value = Undefined)
-
return @#{property_option} if value.equal?(Undefined) # return @unique if value.equal?(Undefined)
-
descendants.each do |descendant| # descendants.each do |descendant|
-
unless descendant.instance_variable_defined?(:@#{property_option}) # unless descendant.instance_variable_defined?(:@unique)
-
descendant.#{property_option}(value) # descendant.unique(value)
-
end # end
-
end # end
-
@#{property_option} = value # @unique = value
-
end # end
-
RUBY
-
end
-
-
20
descendants.each { |descendant| descendant.accepted_options.concat(args) }
-
end
-
-
# @api private
-
1
def nullable(*args)
-
# :required is preferable to :allow_nil, but :nullable maps precisely to :allow_nil
-
raise "#nullable is deprecated, use #required instead (#{caller.first})"
-
end
-
-
# Gives all the options set on this property
-
#
-
# @return [Hash] with all options and their values set on this property
-
#
-
# @api public
-
1
def options
-
35
options = {}
-
35
accepted_options.each do |name|
-
691
options[name] = send(name) if instance_variable_defined?("@#{name}")
-
end
-
35
options
-
end
-
end
-
-
1
accept_options :primitive, *Property::OPTIONS
-
-
# A hook to allow properties to extend or modify the model it's bound to.
-
# Implementations are not supposed to modify the state of the property
-
# class, and should produce no side-effects on the property instance.
-
1
def bind
-
# no op
-
end
-
-
# Supplies the field in the data-store which the property corresponds to
-
#
-
# @return [String] name of field in data-store
-
#
-
# @api semipublic
-
1
def field(repository_name = nil)
-
516
if repository_name
-
raise "Passing in +repository_name+ to #{self.class}#field is deprecated (#{caller.first})"
-
end
-
-
# defer setting the field with the adapter specific naming
-
# conventions until after the adapter has been setup
-
516
@field ||= model.field_naming_convention(self.repository_name).call(self).freeze
-
end
-
-
# Returns true if property is unique. Serial properties and keys
-
# are unique by default.
-
#
-
# @return [Boolean]
-
# true if property has uniq index defined, false otherwise
-
#
-
# @api public
-
1
def unique?
-
!!@unique
-
end
-
-
# Returns index name if property has index.
-
#
-
# @return [Boolean, Symbol, Array]
-
# returns true if property is indexed by itself
-
# returns a Symbol if the property is indexed with other properties
-
# returns an Array if the property belongs to multiple indexes
-
# returns false if the property does not belong to any indexes
-
#
-
# @api public
-
1
attr_reader :index
-
-
# Returns true if property has unique index. Serial properties and
-
# keys are unique by default.
-
#
-
# @return [Boolean, Symbol, Array]
-
# returns true if property is indexed by itself
-
# returns a Symbol if the property is indexed with other properties
-
# returns an Array if the property belongs to multiple indexes
-
# returns false if the property does not belong to any indexes
-
#
-
# @api public
-
1
attr_reader :unique_index
-
-
# Returns whether or not the property is to be lazy-loaded
-
#
-
# @return [Boolean]
-
# true if the property is to be lazy-loaded
-
#
-
# @api public
-
1
def lazy?
-
161
@lazy
-
end
-
-
# Returns whether or not the property is a key or a part of a key
-
#
-
# @return [Boolean]
-
# true if the property is a key or a part of a key
-
#
-
# @api public
-
1
def key?
-
25
@key
-
end
-
-
# Returns whether or not the property is "serial" (auto-incrementing)
-
#
-
# @return [Boolean]
-
# whether or not the property is "serial"
-
#
-
# @api public
-
1
def serial?
-
123
@serial
-
end
-
-
# Returns whether or not the property must be non-nil and non-blank
-
#
-
# @return [Boolean]
-
# whether or not the property is required
-
#
-
# @api public
-
1
def required?
-
231
@required
-
end
-
-
# Returns whether or not the property can accept 'nil' as it's value
-
#
-
# @return [Boolean]
-
# whether or not the property can accept 'nil'
-
#
-
# @api public
-
1
def allow_nil?
-
20
@allow_nil
-
end
-
-
# Returns whether or not the property can be a blank value
-
#
-
# @return [Boolean]
-
# whether or not the property can be blank
-
#
-
# @api public
-
1
def allow_blank?
-
20
@allow_blank
-
end
-
-
# Standardized reader method for the property
-
#
-
# @param [Resource] resource
-
# model instance for which this property is to be loaded
-
#
-
# @return [Object]
-
# the value of this property for the provided instance
-
#
-
# @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
-
#
-
# @api private
-
1
def get(resource)
-
204
get!(resource)
-
end
-
-
# Fetch the ivar value in the resource
-
#
-
# @param [Resource] resource
-
# model instance for which this property is to be unsafely loaded
-
#
-
# @return [Object]
-
# current @ivar value of this property in +resource+
-
#
-
# @api private
-
1
def get!(resource)
-
310
resource.instance_variable_get(instance_variable_name)
-
end
-
-
# Provides a standardized setter method for the property
-
#
-
# @param [Resource] resource
-
# the resource to get the value from
-
# @param [Object] value
-
# the value to set in the resource
-
#
-
# @return [Object]
-
# +value+ after being typecasted according to this property's primitive
-
#
-
# @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
-
#
-
# @api private
-
1
def set(resource, value)
-
99
set!(resource, value)
-
end
-
-
# Set the ivar value in the resource
-
#
-
# @param [Resource] resource
-
# the resource to set
-
# @param [Object] value
-
# the value to set in the resource
-
#
-
# @return [Object]
-
# the value set in the resource
-
#
-
# @api private
-
1
def set!(resource, value)
-
209
resource.instance_variable_set(instance_variable_name, value)
-
end
-
-
# Check if the attribute corresponding to the property is loaded
-
#
-
# @param [Resource] resource
-
# model instance for which the attribute is to be tested
-
#
-
# @return [Boolean]
-
# true if the attribute is loaded in the resource
-
#
-
# @api private
-
1
def loaded?(resource)
-
416
resource.instance_variable_defined?(instance_variable_name)
-
end
-
-
# Loads lazy columns when get or set is called.
-
#
-
# @param [Resource] resource
-
# model instance for which lazy loaded attribute are loaded
-
#
-
# @api private
-
1
def lazy_load(resource)
-
return if loaded?(resource)
-
resource.__send__(:lazy_load, lazy_load_properties)
-
end
-
-
# @api private
-
1
def lazy_load_properties
-
@lazy_load_properties ||=
-
begin
-
properties = self.properties
-
properties.in_context(lazy? ? [ self ] : properties.defaults)
-
end
-
end
-
-
# @api private
-
1
def properties
-
@properties ||= model.properties(repository_name)
-
end
-
-
# @api semipublic
-
1
def typecast(value)
-
319
if value.nil? || primitive?(value)
-
318
value
-
1
elsif respond_to?(:typecast_to_primitive, true)
-
1
typecast_to_primitive(value)
-
else
-
value
-
end
-
end
-
-
# Test the value to see if it is a valid value for this Property
-
#
-
# @param [Object] loaded_value
-
# the value to be tested
-
#
-
# @return [Boolean]
-
# true if the value is valid
-
#
-
# @api semipulic
-
1
def valid?(value, negated = false)
-
231
dumped_value = dump(value)
-
-
231
if required? && dumped_value.nil?
-
33
negated || false
-
else
-
198
primitive?(dumped_value) || (dumped_value.nil? && (allow_nil? || negated))
-
end
-
end
-
-
# Returns a concise string representation of the property instance.
-
#
-
# @return [String]
-
# Concise string representation of the property instance.
-
#
-
# @api public
-
1
def inspect
-
"#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>"
-
end
-
-
# Test a value to see if it matches the primitive type
-
#
-
# @param [Object] value
-
# value to test
-
#
-
# @return [Boolean]
-
# true if the value is the correct type
-
#
-
# @api semipublic
-
1
def primitive?(value)
-
582
value.kind_of?(primitive)
-
end
-
-
1
protected
-
-
# @api semipublic
-
1
def initialize(model, name, options = {})
-
20
options = options.to_hash.dup
-
-
20
if INVALID_NAMES.include?(name.to_s) || (kind_of?(Boolean) && INVALID_NAMES.include?("#{name}?"))
-
raise ArgumentError,
-
"+name+ was #{name.inspect}, which cannot be used as a property name since it collides with an existing method or a query option"
-
end
-
-
20
assert_valid_options(options)
-
-
20
predefined_options = self.class.options
-
-
20
@repository_name = model.repository_name
-
20
@model = model
-
20
@name = name.to_s.chomp('?').to_sym
-
20
@options = predefined_options.merge(options).freeze
-
20
@instance_variable_name = "@#{@name}".freeze
-
-
20
@primitive = self.class.primitive
-
20
@field = @options[:field].freeze unless @options[:field].nil?
-
20
@default = @options[:default]
-
-
20
@serial = @options.fetch(:serial, false)
-
20
@key = @options.fetch(:key, @serial)
-
20
@unique = @options.fetch(:unique, @key ? :key : false)
-
20
@required = @options.fetch(:required, @key)
-
20
@allow_nil = @options.fetch(:allow_nil, !@required)
-
20
@allow_blank = @options.fetch(:allow_blank, !@required)
-
20
@index = @options.fetch(:index, false)
-
20
@unique_index = @options.fetch(:unique_index, @unique)
-
20
@lazy = @options.fetch(:lazy, false) && !@key
-
-
20
determine_visibility
-
-
20
bind
-
end
-
-
# @api private
-
1
def assert_valid_options(options)
-
20
keys = options.keys
-
-
20
if (unknown_keys = keys - self.class.accepted_options).any?
-
raise ArgumentError, "options #{unknown_keys.map { |key| key.inspect }.join(' and ')} are unknown"
-
end
-
-
20
options.each do |key, value|
-
47
boolean_value = value == true || value == false
-
-
47
case key
-
when :field
-
assert_kind_of "options[:#{key}]", value, ::String
-
-
when :default
-
if value.nil?
-
raise ArgumentError, "options[:#{key}] must not be nil"
-
end
-
-
when :serial, :key, :allow_nil, :allow_blank, :required, :auto_validation
-
20
unless boolean_value
-
raise ArgumentError, "options[:#{key}] must be either true or false"
-
end
-
-
20
if key == :required && (keys.include?(:allow_nil) || keys.include?(:allow_blank))
-
raise ArgumentError, 'options[:required] cannot be mixed with :allow_nil or :allow_blank'
-
end
-
-
when :index, :unique_index, :unique, :lazy
-
13
unless boolean_value || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.any? && value.all? { |val| val.kind_of?(Symbol) })
-
raise ArgumentError, "options[:#{key}] must be either true, false, a Symbol or an Array of Symbols"
-
end
-
-
when :length
-
1
assert_kind_of "options[:#{key}]", value, Range, ::Integer
-
-
when :size, :precision, :scale
-
assert_kind_of "options[:#{key}]", value, ::Integer
-
-
when :reader, :writer, :accessor
-
assert_kind_of "options[:#{key}]", value, Symbol
-
-
unless VISIBILITY_OPTIONS.include?(value)
-
raise ArgumentError, "options[:#{key}] must be #{VISIBILITY_OPTIONS.join(' or ')}"
-
end
-
end
-
end
-
end
-
-
# Assert given visibility value is supported.
-
#
-
# Will raise ArgumentError if this Property's reader and writer
-
# visibilities are not included in VISIBILITY_OPTIONS.
-
#
-
# @return [undefined]
-
#
-
# @raise [ArgumentError] "property visibility must be :public, :protected, or :private"
-
#
-
# @api private
-
1
def determine_visibility
-
20
default_accessor = @options.fetch(:accessor, :public)
-
-
20
@reader_visibility = @options.fetch(:reader, default_accessor)
-
20
@writer_visibility = @options.fetch(:writer, default_accessor)
-
end
-
end # class Property
-
end
-
1
module DataMapper
-
1
class Property
-
1
class Binary < String
-
1
include PassThroughLoadDump
-
-
1
if RUBY_VERSION >= "1.9"
-
-
1
def load(value)
-
super.dup.force_encoding("BINARY") unless value.nil?
-
end
-
-
1
def dump(value)
-
value.dup.force_encoding("BINARY") unless value.nil?
-
rescue
-
value
-
end
-
-
end
-
-
end # class Binary
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Boolean < Object
-
1
include PassThroughLoadDump
-
-
1
primitive ::TrueClass
-
-
1
TRUE_VALUES = [ 1, '1', 't', 'T', 'true', 'TRUE' ].freeze
-
1
FALSE_VALUES = [ 0, '0', 'f', 'F', 'false', 'FALSE' ].freeze
-
1
BOOLEAN_MAP = Hash[
-
TRUE_VALUES.product([ true ]) + FALSE_VALUES.product([ false ]) ].freeze
-
-
1
def primitive?(value)
-
value == true || value == false
-
end
-
-
# Typecast a value to a true or false
-
#
-
# @param [Integer, #to_str] value
-
# value to typecast
-
#
-
# @return [Boolean]
-
# true or false constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
BOOLEAN_MAP.fetch(value, value)
-
end
-
end # class Boolean
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Class < Object
-
1
include PassThroughLoadDump
-
-
1
primitive ::Class
-
-
# Typecast a value to a Class
-
#
-
# @param [#to_s] value
-
# value to typecast
-
#
-
# @return [Class]
-
# Class constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
DataMapper::Ext::Module.find_const(model, value.to_s)
-
rescue NameError
-
value
-
end
-
end # class Class
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Date < Object
-
1
include PassThroughLoadDump
-
1
include Typecast::Time
-
-
1
primitive ::Date
-
-
# Typecasts an arbitrary value to a Date
-
# Handles both Hashes and Date instances.
-
#
-
# @param [Hash, #to_mash, #to_s] value
-
# value to be typecast
-
#
-
# @return [Date]
-
# Date constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
if value.respond_to?(:to_date)
-
value.to_date
-
elsif value.is_a?(::Hash) || value.respond_to?(:to_mash)
-
typecast_hash_to_date(value)
-
else
-
::Date.parse(value.to_s)
-
end
-
rescue ArgumentError
-
value
-
end
-
-
# Creates a Date instance from a Hash with keys :year, :month, :day
-
#
-
# @param [Hash, #to_mash] value
-
# value to be typecast
-
#
-
# @return [Date]
-
# Date constructed from hash
-
#
-
# @api private
-
1
def typecast_hash_to_date(value)
-
::Date.new(*extract_time(value)[0, 3])
-
end
-
end # class Date
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class DateTime < Object
-
1
include PassThroughLoadDump
-
1
include Typecast::Time
-
-
1
primitive ::DateTime
-
-
# Typecasts an arbitrary value to a DateTime.
-
# Handles both Hashes and DateTime instances.
-
#
-
# @param [Hash, #to_mash, #to_s] value
-
# value to be typecast
-
#
-
# @return [DateTime]
-
# DateTime constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
if value.is_a?(::Hash) || value.respond_to?(:to_mash)
-
typecast_hash_to_datetime(value)
-
else
-
::DateTime.parse(value.to_s)
-
end
-
rescue ArgumentError
-
value
-
end
-
-
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
-
# :hour, :min, :sec
-
#
-
# @param [Hash, #to_mash] value
-
# value to be typecast
-
#
-
# @return [DateTime]
-
# DateTime constructed from hash
-
#
-
# @api private
-
1
def typecast_hash_to_datetime(value)
-
::DateTime.new(*extract_time(value))
-
end
-
end # class DateTime
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Decimal < Numeric
-
1
primitive BigDecimal
-
-
1
DEFAULT_PRECISION = 10
-
1
DEFAULT_SCALE = 0
-
-
1
precision(DEFAULT_PRECISION)
-
1
scale(DEFAULT_SCALE)
-
-
1
protected
-
-
1
def initialize(model, name, options = {})
-
super
-
-
[ :scale, :precision ].each do |key|
-
unless @options.key?(key)
-
warn "options[#{key.inspect}] should be set for #{self.class}, defaulting to #{send(key).inspect} (#{caller.first})"
-
end
-
end
-
-
unless @scale >= 0
-
raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
-
end
-
-
unless @precision >= @scale
-
raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}"
-
end
-
end
-
-
# Typecast a value to a BigDecimal
-
#
-
# @param [#to_str, #to_d, Integer] value
-
# value to typecast
-
#
-
# @return [BigDecimal]
-
# BigDecimal constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
if value.kind_of?(::Integer)
-
value.to_s.to_d
-
else
-
typecast_to_numeric(value, :to_d)
-
end
-
end
-
end # class Decimal
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Discriminator < Class
-
1
include PassThroughLoadDump
-
-
1
default lambda { |resource, property| resource.model }
-
1
required true
-
-
# @api private
-
1
def bind
-
model.extend Model unless model < Model
-
end
-
-
1
module Model
-
1
def inherited(model)
-
super # setup self.descendants
-
set_discriminator_scope_for(model)
-
end
-
-
1
def new(*args, &block)
-
if args.size == 1 && args.first.kind_of?(Hash)
-
discriminator = properties(repository_name).discriminator
-
-
if discriminator_value = args.first[discriminator.name]
-
model = discriminator.typecast_to_primitive(discriminator_value)
-
-
if model.kind_of?(Model) && !model.equal?(self)
-
return model.new(*args, &block)
-
end
-
end
-
end
-
-
super
-
end
-
-
1
private
-
-
1
def set_discriminator_scope_for(model)
-
discriminator = self.properties.discriminator
-
default_scope = model.default_scope(discriminator.repository_name)
-
default_scope.update(discriminator.name => model.descendants.dup << model)
-
end
-
end
-
end # class Discriminator
-
end # module Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Float < Numeric
-
1
primitive ::Float
-
-
1
DEFAULT_PRECISION = 10
-
1
DEFAULT_SCALE = nil
-
-
1
precision(DEFAULT_PRECISION)
-
1
scale(DEFAULT_SCALE)
-
-
1
protected
-
-
# Typecast a value to a Float
-
#
-
# @param [#to_str, #to_f] value
-
# value to typecast
-
#
-
# @return [Float]
-
# Float constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
typecast_to_numeric(value, :to_f)
-
end
-
end # class Float
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Integer < Numeric
-
1
primitive ::Integer
-
-
1
accept_options :serial
-
-
1
protected
-
-
# @api semipublic
-
1
def initialize(model, name, options = {})
-
11
if options.key?(:serial) && !kind_of?(Serial)
-
raise "Integer #{name} with explicit :serial option is deprecated, use Serial instead (#{caller[2]})"
-
end
-
11
super
-
end
-
-
# Typecast a value to an Integer
-
#
-
# @param [#to_str, #to_i] value
-
# value to typecast
-
#
-
# @return [Integer]
-
# Integer constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
1
typecast_to_numeric(value, :to_i)
-
end
-
end # class Integer
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
module Lookup
-
-
1
protected
-
-
#
-
# Provides transparent access to the Properties defined in
-
# {Property}.
-
#
-
# @param [Symbol] name
-
# The name of the property to lookup.
-
#
-
# @return [Property]
-
# The property with the given name.
-
#
-
# @raise [NameError]
-
# The property could not be found.
-
#
-
# @api private
-
#
-
# @since 1.0.1
-
#
-
1
def const_missing(name)
-
9
Property.find_class(name.to_s) || super
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
1
class Property
-
1
class Numeric < Object
-
1
include PassThroughLoadDump
-
1
include Typecast::Numeric
-
-
1
accept_options :precision, :scale, :min, :max
-
1
attr_reader :precision, :scale, :min, :max
-
-
1
DEFAULT_NUMERIC_MIN = 0
-
1
DEFAULT_NUMERIC_MAX = 2**31-1
-
-
1
protected
-
-
1
def initialize(model, name, options = {})
-
11
super
-
-
11
if @primitive == BigDecimal || @primitive == ::Float
-
@precision = @options.fetch(:precision)
-
@scale = @options.fetch(:scale)
-
-
unless @precision > 0
-
raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
-
end
-
end
-
-
11
if @options.key?(:min) || @options.key?(:max)
-
10
@min = @options.fetch(:min, self.class::DEFAULT_NUMERIC_MIN)
-
10
@max = @options.fetch(:max, self.class::DEFAULT_NUMERIC_MAX)
-
-
10
if @max < DEFAULT_NUMERIC_MIN && !@options.key?(:min)
-
raise ArgumentError, "min should be specified when the max is less than #{DEFAULT_NUMERIC_MIN}"
-
10
elsif @max < @min
-
raise ArgumentError, "max must be less than the min, but was #{@max} while the min was #{@min}"
-
end
-
end
-
end
-
end # class Numeric
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Object < Property
-
1
primitive ::Object
-
-
# @api semipublic
-
1
def dump(value)
-
return if value.nil?
-
[ Marshal.dump(value) ].pack('m')
-
end
-
-
# @api semipublic
-
1
def load(value)
-
case value
-
when ::String
-
Marshal.load(value.unpack('m').first)
-
when ::Object
-
value
-
end
-
end
-
-
# @api private
-
1
def to_child_key
-
self.class
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
1
class Property
-
1
class Serial < Integer
-
1
serial true
-
1
min 1
-
-
# @api private
-
1
def to_child_key
-
6
Property::Integer
-
end
-
end # class Text
-
end # module Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class String < Object
-
1
include PassThroughLoadDump
-
-
1
primitive ::String
-
-
1
accept_options :length
-
-
1
DEFAULT_LENGTH = 50
-
1
length(DEFAULT_LENGTH)
-
-
# Returns maximum property length (if applicable).
-
# This usually only makes sense when property is of
-
# type Range or custom
-
#
-
# @return [Integer, nil]
-
# the maximum length of this property
-
#
-
# @api semipublic
-
1
def length
-
5
if @length.kind_of?(Range)
-
@length.max
-
else
-
5
@length
-
end
-
end
-
-
1
protected
-
-
1
def initialize(model, name, options = {})
-
7
super
-
7
@length = @options.fetch(:length)
-
end
-
-
# Typecast a value to a String
-
#
-
# @param [#to_s] value
-
# value to typecast
-
#
-
# @return [String]
-
# String constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
value.to_s
-
end
-
end # class String
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Text < String
-
1
length 65535
-
1
lazy true
-
-
1
def primitive?(value)
-
48
value.kind_of?(::String)
-
end
-
end # class Text
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
class Time < Object
-
1
include PassThroughLoadDump
-
1
include Typecast::Time
-
-
1
primitive ::Time
-
-
# Typecasts an arbitrary value to a Time
-
# Handles both Hashes and Time instances.
-
#
-
# @param [Hash, #to_mash, #to_s] value
-
# value to be typecast
-
#
-
# @return [Time]
-
# Time constructed from value
-
#
-
# @api private
-
1
def typecast_to_primitive(value)
-
if value.respond_to?(:to_time)
-
value.to_time
-
elsif value.is_a?(::Hash) || value.respond_to?(:to_mash)
-
typecast_hash_to_time(value)
-
else
-
::Time.parse(value.to_s)
-
end
-
rescue ArgumentError
-
value
-
end
-
-
# Creates a Time instance from a Hash with keys :year, :month, :day,
-
# :hour, :min, :sec
-
#
-
# @param [Hash, #to_mash] value
-
# value to be typecast
-
#
-
# @return [Time]
-
# Time constructed from hash
-
#
-
# @api private
-
1
def typecast_hash_to_time(value)
-
::Time.local(*extract_time(value))
-
end
-
end # class Time
-
end # class Property
-
end # module DataMapper
-
1
module DataMapper
-
1
class Property
-
1
module Typecast
-
1
module Numeric
-
# Match numeric string
-
#
-
# @param [#to_str, Numeric] value
-
# value to typecast
-
# @param [Symbol] method
-
# method to typecast with
-
#
-
# @return [Numeric]
-
# number if matched, value if no match
-
#
-
# @api private
-
1
def typecast_to_numeric(value, method)
-
1
if value.respond_to?(:to_str)
-
1
if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
-
1
$1.send(method)
-
else
-
value
-
end
-
elsif value.respond_to?(method)
-
value.send(method)
-
else
-
value
-
end
-
end
-
end # Numeric
-
end # Typecast
-
end # Property
-
end # DataMapper
-
1
module DataMapper
-
1
class Property
-
1
module Typecast
-
1
module Time
-
1
include Numeric
-
-
# Extracts the given args from the hash. If a value does not exist, it
-
# uses the value of Time.now.
-
#
-
# @param [Hash, #to_mash] value
-
# value to extract time args from
-
#
-
# @return [Array]
-
# Extracted values
-
#
-
# @api private
-
1
def extract_time(value)
-
mash = if value.respond_to?(:to_mash)
-
value.to_mash
-
else
-
DataMapper::Ext::Hash.to_mash(value)
-
end
-
-
now = ::Time.now
-
-
[ :year, :month, :day, :hour, :min, :sec ].map do |segment|
-
typecast_to_numeric(mash.fetch(segment, now.send(segment)), :to_i)
-
end
-
end
-
end # Time
-
end # Typecast
-
end # Property
-
end # DataMapper
-
1
module DataMapper
-
# Set of Property objects, used to associate
-
# queries with set of fields it performed over,
-
# to represent composite keys (esp. for associations)
-
# and so on.
-
1
class PropertySet < SubjectSet
-
1
include Enumerable
-
-
1
def <<(property)
-
20
clear_cache
-
20
super
-
end
-
-
# Make sure that entry is part of this PropertySet
-
#
-
# @param [#to_s] name
-
# @param [#name] entry
-
#
-
# @return [#name]
-
# the entry that is now part of this PropertySet
-
#
-
# @api semipublic
-
1
def []=(name, entry)
-
warn "#{self.class}#[]= is deprecated. Use #{self.class}#<< instead: #{caller.first}"
-
raise "#{entry.class} is not added with the correct name" unless name && name.to_s == entry.name.to_s
-
self << entry
-
entry
-
end
-
-
1
def |(other)
-
26
self.class.new(to_a | other.to_a)
-
end
-
-
1
def &(other)
-
self.class.new(to_a & other.to_a)
-
end
-
-
1
def -(other)
-
4
self.class.new(to_a - other.to_a)
-
end
-
-
1
def +(other)
-
self.class.new(to_a + other.to_a)
-
end
-
-
1
def ==(other)
-
to_a == other.to_a
-
end
-
-
# TODO: make PropertySet#reject return a PropertySet instance
-
# @api semipublic
-
1
def defaults
-
159
@defaults ||= self.class.new(key | [ discriminator ].compact | reject { |property| property.lazy? }).freeze
-
end
-
-
# @api semipublic
-
1
def key
-
320
@key ||= self.class.new(select { |property| property.key? }).freeze
-
end
-
-
# @api semipublic
-
1
def discriminator
-
523
@discriminator ||= detect { |property| property.kind_of?(Property::Discriminator) }
-
end
-
-
# @api semipublic
-
1
def indexes
-
index_hash = {}
-
each { |property| parse_index(property.index, property.field, index_hash) }
-
index_hash
-
end
-
-
# @api semipublic
-
1
def unique_indexes
-
index_hash = {}
-
each { |property| parse_index(property.unique_index, property.field, index_hash) }
-
index_hash
-
end
-
-
# @api semipublic
-
1
def get(resource)
-
11
return [] if resource.nil?
-
22
map { |property| resource.__send__(property.name) }
-
end
-
-
# @api semipublic
-
1
def get!(resource)
-
16
map { |property| property.get!(resource) }
-
end
-
-
# @api semipublic
-
1
def set(resource, values)
-
22
zip(values) { |property, value| resource.__send__("#{property.name}=", value) }
-
end
-
-
# @api semipublic
-
1
def set!(resource, values)
-
zip(values) { |property, value| property.set!(resource, value) }
-
end
-
-
# @api semipublic
-
1
def loaded?(resource)
-
all? { |property| property.loaded?(resource) }
-
end
-
-
# @api semipublic
-
1
def valid?(values)
-
174
zip(values.nil? ? [] : values).all? { |property, value| property.valid?(value) }
-
end
-
-
# @api semipublic
-
1
def typecast(values)
-
58
zip(values.nil? ? [] : values).map { |property, value| property.typecast(value) }
-
end
-
-
# @api private
-
1
def property_contexts(property)
-
contexts = []
-
lazy_contexts.each do |context, properties|
-
contexts << context if properties.include?(property)
-
end
-
contexts
-
end
-
-
# @api private
-
1
def lazy_context(context)
-
2
lazy_contexts[context] ||= []
-
end
-
-
# @api private
-
1
def in_context(properties)
-
properties_in_context = properties.map do |property|
-
if (contexts = property_contexts(property)).any?
-
lazy_contexts.values_at(*contexts)
-
else
-
property
-
end
-
end
-
-
properties_in_context.flatten.uniq
-
end
-
-
# @api private
-
1
def field_map
-
Hash[ map { |property| [ property.field, property ] } ]
-
end
-
-
1
def inspect
-
to_a.inspect
-
end
-
-
1
private
-
-
# @api private
-
1
def clear_cache
-
20
@defaults, @key, @discriminator = nil
-
end
-
-
# @api private
-
1
def lazy_contexts
-
2
@lazy_contexts ||= {}
-
end
-
-
# @api private
-
1
def parse_index(index, property, index_hash)
-
case index
-
when true
-
index_hash[property] = [ property ]
-
when Symbol
-
index_hash[index] ||= []
-
index_hash[index] << property
-
when Array
-
index.each { |idx| parse_index(idx, property, index_hash) }
-
end
-
end
-
end # class PropertySet
-
end # module DataMapper
-
# TODO: break this up into classes for each primary option, eg:
-
#
-
# - DataMapper::Query::Fields
-
# - DataMapper::Query::Links
-
# - DataMapper::Query::Conditions
-
# - DataMapper::Query::Offset
-
# - DataMapper::Query::Limit
-
# - DataMapper::Query::Order
-
#
-
# TODO: move assertions, validations, transformations, and equality
-
# checking into each class and clean up Query
-
#
-
# TODO: add a way to "register" these classes with the Query object
-
# so that new reserved options can be added in the future. Each
-
# class will need to implement a "slug" method or something similar
-
# so that their option namespace can be reserved.
-
-
# TODO: move condition transformations into a Query::Conditions
-
# helper class that knows how to transform the primitives, and
-
# calls #comparison_for(repository, model) on objects (or some
-
# other convention that we establish)
-
-
1
module DataMapper
-
-
# Query class represents a query which will be run against the data-store.
-
# Generally Query objects can be found inside Collection objects.
-
#
-
1
class Query
-
1
include DataMapper::Assertions
-
1
extend Equalizer
-
-
1
OPTIONS = [ :fields, :links, :conditions, :offset, :limit, :order, :unique, :add_reversed, :reload ].to_set.freeze
-
-
1
equalize :repository, :model, :sorted_fields, :links, :conditions, :order, :offset, :limit, :reload?, :unique?, :add_reversed?
-
-
# Extract conditions to match a Resource or Collection
-
#
-
# @param [Array, Collection, Resource] source
-
# the source to extract the values from
-
# @param [ProperySet] source_key
-
# the key to extract the value from the resource
-
# @param [ProperySet] target_key
-
# the key to match the resource with
-
#
-
# @return [AbstractComparison, AbstractOperation]
-
# the conditions to match the resources with
-
#
-
# @api private
-
1
def self.target_conditions(source, source_key, target_key)
-
target_key_size = target_key.size
-
source_values = []
-
-
if source.nil?
-
source_values << [ nil ] * target_key_size
-
else
-
Array(source).each do |resource|
-
next unless source_key.loaded?(resource)
-
source_value = source_key.get!(resource)
-
next unless target_key.valid?(source_value)
-
source_values << source_value
-
end
-
end
-
-
source_values.uniq!
-
-
if target_key_size == 1
-
target_key = target_key.first
-
source_values.flatten!
-
-
if source_values.size == 1
-
Conditions::EqualToComparison.new(target_key, source_values.first)
-
else
-
Conditions::InclusionComparison.new(target_key, source_values)
-
end
-
else
-
or_operation = Conditions::OrOperation.new
-
-
source_values.each do |source_value|
-
and_operation = Conditions::AndOperation.new
-
-
target_key.zip(source_value) do |property, value|
-
and_operation << Conditions::EqualToComparison.new(property, value)
-
end
-
-
or_operation << and_operation
-
end
-
-
or_operation
-
end
-
end
-
-
# @param [Repository] repository
-
# the default repository to scope the query within
-
# @param [Model] model
-
# the default model for the query
-
# @param [#query, Enumerable] source
-
# the source to generate the query with
-
#
-
# @return [Query]
-
# the query to match the resources with
-
#
-
# @api private
-
1
def self.target_query(repository, model, source)
-
if source.respond_to?(:query)
-
source.query
-
elsif source.kind_of?(Enumerable)
-
key = model.key(repository.name)
-
conditions = Query.target_conditions(source, key, key)
-
repository.new_query(model, :conditions => conditions)
-
else
-
raise ArgumentError, "+source+ must respond to #query or be an Enumerable, but was #{source.class}"
-
end
-
end
-
-
# Returns the repository query should be
-
# executed in
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:repository => :medline)
-
#
-
#
-
# @return [Repository]
-
# the Repository to retrieve results from
-
#
-
# @api semipublic
-
1
attr_reader :repository
-
-
# Returns model (class) that is used
-
# to instantiate objects from query result
-
# returned by adapter
-
#
-
# @return [Model]
-
# the Model to retrieve results from
-
#
-
# @api semipublic
-
1
attr_reader :model
-
-
# Returns the fields
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:fields => [:title, :vernacular_title, :abstract])
-
#
-
# @return [PropertySet]
-
# the properties in the Model that will be retrieved
-
#
-
# @api semipublic
-
1
attr_reader :fields
-
-
# Returns the links (associations) query fetches
-
#
-
# @return [Array<DataMapper::Associations::Relationship>]
-
# the relationships that will be used to scope the results
-
#
-
# @api private
-
1
attr_reader :links
-
-
# Returns the conditions of the query
-
#
-
# In the following example:
-
#
-
# @example
-
#
-
# Team.all(:wins.gt => 30, :conference => 'East')
-
#
-
# Conditions are "greater than" operator for "wins"
-
# field and exact match operator for "conference".
-
#
-
# @return [Array]
-
# the conditions that will be used to scope the results
-
#
-
# @api semipublic
-
1
attr_reader :conditions
-
-
# Returns the offset query uses
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:offset => page.offset)
-
#
-
# @return [Integer]
-
# the offset of the results
-
#
-
# @api semipublic
-
1
attr_reader :offset
-
-
# Returns the limit query uses
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:limit => 10)
-
#
-
# @return [Integer, nil]
-
# the maximum number of results
-
#
-
# @api semipublic
-
1
attr_reader :limit
-
-
# Returns the order
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:order => [:created_at.desc, :length.desc])
-
#
-
# query order is a set of two ordering rules, descending on
-
# "created_at" field and descending again on "length" field
-
#
-
# @return [Array]
-
# the order of results
-
#
-
# @api semipublic
-
1
attr_reader :order
-
-
# Returns the original options
-
#
-
# @return [Hash]
-
# the original options
-
#
-
# @api private
-
1
attr_reader :options
-
-
# Indicates if each result should be returned in reverse order
-
#
-
# Set in cases like the following:
-
#
-
# @example
-
#
-
# Document.all(:limit => 5).reverse
-
#
-
# Note that :add_reversed option may be used in conditions directly,
-
# but this is rarely the case
-
#
-
# @return [Boolean]
-
# true if the results should be reversed, false if not
-
#
-
# @api private
-
1
def add_reversed?
-
@add_reversed
-
end
-
-
# Indicates if the Query results should replace the results in the Identity Map
-
#
-
# TODO: needs example
-
#
-
# @return [Boolean]
-
# true if the results should be reloaded, false if not
-
#
-
# @api semipublic
-
1
def reload?
-
83
@reload
-
end
-
-
# Indicates if the Query results should be unique
-
#
-
# TODO: needs example
-
#
-
# @return [Boolean]
-
# true if the results should be unique, false if not
-
#
-
# @api semipublic
-
1
def unique?
-
95
@unique
-
end
-
-
# Indicates if the Query has raw conditions
-
#
-
# @return [Boolean]
-
# true if the query has raw conditions, false if not
-
#
-
# @api semipublic
-
1
def raw?
-
@raw
-
end
-
-
# Indicates if the Query is valid
-
#
-
# @return [Boolean]
-
# true if the query is valid
-
#
-
# @api semipublic
-
1
def valid?
-
112
conditions.valid?
-
end
-
-
# Returns a new Query with a reversed order
-
#
-
# @example
-
#
-
# Document.all(:limit => 5).reverse
-
#
-
# Will execute a single query with correct order
-
#
-
# @return [Query]
-
# new Query with reversed order
-
#
-
# @api semipublic
-
1
def reverse
-
dup.reverse!
-
end
-
-
# Reverses the sort order of the Query
-
#
-
# @example
-
#
-
# Document.all(:limit => 5).reverse
-
#
-
# Will execute a single query with original order
-
# and then reverse collection in the Ruby space
-
#
-
# @return [Query]
-
# self
-
#
-
# @api semipublic
-
1
def reverse!
-
# reverse the sort order
-
@order.map! { |direction| direction.dup.reverse! }
-
-
# copy the order to the options
-
@options = @options.merge(:order => @order).freeze
-
-
self
-
end
-
-
# Updates the Query with another Query or conditions
-
#
-
# Pretty unrealistic example:
-
#
-
# @example
-
#
-
# Journal.all(:limit => 2).query.limit # => 2
-
# Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
-
#
-
# @param [Query, Hash] other
-
# other Query or conditions
-
#
-
# @return [Query]
-
# self
-
#
-
# @api semipublic
-
1
def update(other)
-
236
other_options = if kind_of?(other.class)
-
return self if self.eql?(other)
-
assert_valid_other(other)
-
other.options
-
else
-
236
other = other.to_hash
-
236
return self if other.empty?
-
231
other
-
end
-
-
231
@options = @options.merge(other_options).freeze
-
231
assert_valid_options(@options)
-
-
231
normalize = DataMapper::Ext::Hash.only(other_options, *OPTIONS - [ :conditions ]).map do |attribute, value|
-
334
instance_variable_set("@#{attribute}", DataMapper::Ext.try_dup(value))
-
334
attribute
-
end
-
-
231
merge_conditions([ DataMapper::Ext::Hash.except(other_options, *OPTIONS), other_options[:conditions] ])
-
231
normalize_options(normalize | [ :links, :unique ])
-
-
231
self
-
end
-
-
# Similar to Query#update, but acts on a duplicate.
-
#
-
# @param [Query, Hash] other
-
# other query to merge with
-
#
-
# @return [Query]
-
# updated duplicate of original query
-
#
-
# @api semipublic
-
1
def merge(other)
-
12
dup.update(other)
-
end
-
-
# Builds and returns new query that merges
-
# original with one given, and slices the result
-
# with respect to :limit and :offset options
-
#
-
# This method is used by Collection to
-
# concatenate options from multiple chained
-
# calls in cases like the following:
-
#
-
# @example
-
#
-
# author.books.all(:year => 2009).all(:published => false)
-
#
-
# @api semipublic
-
1
def relative(options)
-
options = options.to_hash
-
-
offset = nil
-
limit = self.limit
-
-
if options.key?(:offset) && (options.key?(:limit) || limit)
-
options = options.dup
-
offset = options.delete(:offset)
-
limit = options.delete(:limit) || limit - offset
-
end
-
-
query = merge(options)
-
query = query.slice!(offset, limit) if offset
-
query
-
end
-
-
# Return the union with another query
-
#
-
# @param [Query] other
-
# the other query
-
#
-
# @return [Query]
-
# the union of the query and other
-
#
-
# @api semipublic
-
1
def union(other)
-
return dup if self == other
-
set_operation(:union, other)
-
end
-
-
1
alias_method :|, :union
-
1
alias_method :+, :union
-
-
# Return the intersection with another query
-
#
-
# @param [Query] other
-
# the other query
-
#
-
# @return [Query]
-
# the intersection of the query and other
-
#
-
# @api semipublic
-
1
def intersection(other)
-
return dup if self == other
-
set_operation(:intersection, other)
-
end
-
-
1
alias_method :&, :intersection
-
-
# Return the difference with another query
-
#
-
# @param [Query] other
-
# the other query
-
#
-
# @return [Query]
-
# the difference of the query and other
-
#
-
# @api semipublic
-
1
def difference(other)
-
set_operation(:difference, other)
-
end
-
-
1
alias_method :-, :difference
-
-
# Clear conditions
-
#
-
# @return [self]
-
#
-
# @api semipublic
-
1
def clear
-
@conditions = Conditions::Operation.new(:null)
-
self
-
end
-
-
# Takes an Enumerable of records, and destructively filters it.
-
# First finds all matching conditions, then sorts it,
-
# then does offset & limit
-
#
-
# @param [Enumerable] records
-
# The set of records to be filtered
-
#
-
# @return [Enumerable]
-
# Whats left of the given array after the filtering
-
#
-
# @api semipublic
-
1
def filter_records(records)
-
records = records.uniq if unique?
-
records = match_records(records) if conditions
-
records = sort_records(records) if order
-
records = limit_records(records) if limit || offset > 0
-
records
-
end
-
-
# Filter a set of records by the conditions
-
#
-
# @param [Enumerable] records
-
# The set of records to be filtered
-
#
-
# @return [Enumerable]
-
# Whats left of the given array after the matching
-
#
-
# @api semipublic
-
1
def match_records(records)
-
conditions = self.conditions
-
records.select { |record| conditions.matches?(record) }
-
end
-
-
# Sorts a list of Records by the order
-
#
-
# @param [Enumerable] records
-
# A list of Resources to sort
-
#
-
# @return [Enumerable]
-
# The sorted records
-
#
-
# @api semipublic
-
1
def sort_records(records)
-
sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
-
-
records.sort_by do |record|
-
sort_order.map do |(property, ascending)|
-
Sort.new(record_value(record, property), ascending)
-
end
-
end
-
end
-
-
# Limits a set of records by the offset and/or limit
-
#
-
# @param [Enumerable] records
-
# A list of records to sort
-
#
-
# @return [Enumerable]
-
# The offset & limited records
-
#
-
# @api semipublic
-
1
def limit_records(records)
-
offset = self.offset
-
limit = self.limit
-
size = records.size
-
-
if offset > size - 1
-
[]
-
elsif (limit && limit != size) || offset > 0
-
records[offset, limit || size] || []
-
else
-
records.dup
-
end
-
end
-
-
# Slices collection by adding limit and offset to the
-
# query, so a single query is executed
-
#
-
# @example
-
#
-
# Journal.all(:limit => 10).slice(3, 5)
-
#
-
# will execute query with the following limit and offset
-
# (when repository uses DataObjects adapter, and thus
-
# queries use SQL):
-
#
-
# LIMIT 5 OFFSET 3
-
#
-
# @api semipublic
-
1
def slice(*args)
-
100
dup.slice!(*args)
-
end
-
-
1
alias_method :[], :slice
-
-
# Slices collection by adding limit and offset to the
-
# query, so a single query is executed
-
#
-
# @example
-
#
-
# Journal.all(:limit => 10).slice!(3, 5)
-
#
-
# will execute query with the following limit
-
# (when repository uses DataObjects adapter, and thus
-
# queries use SQL):
-
#
-
# LIMIT 10
-
#
-
# and then takes a slice of collection in the Ruby space
-
#
-
# @api semipublic
-
1
def slice!(*args)
-
100
offset, limit = extract_slice_arguments(*args)
-
-
100
if self.limit || self.offset > 0
-
offset, limit = get_relative_position(offset, limit)
-
end
-
-
100
update(:offset => offset, :limit => limit)
-
end
-
-
# Returns detailed human readable
-
# string representation of the query
-
#
-
# @return [String] detailed string representation of the query
-
#
-
# @api semipublic
-
1
def inspect
-
attrs = [
-
[ :repository, repository.name ],
-
[ :model, model ],
-
[ :fields, fields ],
-
[ :links, links ],
-
[ :conditions, conditions ],
-
[ :order, order ],
-
[ :limit, limit ],
-
[ :offset, offset ],
-
[ :reload, reload? ],
-
[ :unique, unique? ],
-
]
-
-
"#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
-
end
-
-
# Get the properties used in the conditions
-
#
-
# @return [Set<Property>]
-
# Set of properties used in the conditions
-
#
-
# @api private
-
1
def condition_properties
-
properties = Set.new
-
-
each_comparison do |comparison|
-
next unless comparison.respond_to?(:subject)
-
subject = comparison.subject
-
properties << subject if subject.kind_of?(Property)
-
end
-
-
properties
-
end
-
-
# Return a list of fields in predictable order
-
#
-
# @return [Array<Property>]
-
# list of fields sorted in deterministic order
-
#
-
# @api private
-
1
def sorted_fields
-
fields.sort_by { |property| property.hash }
-
end
-
-
# Transform Query into subquery conditions
-
#
-
# @return [AndOperation]
-
# a subquery for the Query
-
#
-
# @api private
-
1
def to_subquery
-
collection = model.all(merge(:fields => model_key))
-
Conditions::Operation.new(:and, Conditions::Comparison.new(:in, self_relationship, collection))
-
end
-
-
# Hash representation of a Query
-
#
-
# @return [Hash]
-
# Hash representation of a Query
-
#
-
# @api private
-
1
def to_hash
-
{
-
:repository => repository.name,
-
:model => model.name,
-
:fields => fields,
-
:links => links,
-
:conditions => conditions,
-
:offset => offset,
-
:limit => limit,
-
:order => order,
-
:unique => unique?,
-
:add_reversed => add_reversed?,
-
:reload => reload?,
-
}
-
end
-
-
# Extract options from a Query
-
#
-
# @param [Query] query
-
# the query to extract options from
-
#
-
# @return [Hash]
-
# the options to use to initialize the new query
-
#
-
# @api private
-
1
def to_relative_hash
-
DataMapper::Ext::Hash.only(to_hash, :fields, :order, :unique, :add_reversed, :reload)
-
end
-
-
1
private
-
-
# Initializes a Query instance
-
#
-
# @example
-
#
-
# JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
-
#
-
# initialized a query with repository defined with name :medline,
-
# model JournalIssue and options { :created_on.gte => Date.today - 7 }
-
#
-
# @param [Repository] repository
-
# the Repository to retrieve results from
-
# @param [Model] model
-
# the Model to retrieve results from
-
# @param [Hash] options
-
# the conditions and scope
-
#
-
# @api semipublic
-
1
def initialize(repository, model, options = {})
-
146
assert_kind_of 'repository', repository, Repository
-
146
assert_kind_of 'model', model, Model
-
-
146
@repository = repository
-
146
@model = model
-
146
@options = options.dup.freeze
-
-
146
repository_name = repository.name
-
-
146
@properties = @model.properties(repository_name)
-
146
@relationships = @model.relationships(repository_name)
-
-
146
assert_valid_options(@options)
-
-
146
@fields = @options.fetch :fields, @properties.defaults
-
146
@links = @options.key?(:links) ? @options[:links].dup : []
-
146
@conditions = Conditions::Operation.new(:null)
-
146
@offset = @options.fetch :offset, 0
-
146
@limit = @options.fetch :limit, nil
-
146
@order = @options.fetch :order, @model.default_order(repository_name)
-
146
@unique = @options.fetch :unique, true
-
146
@add_reversed = @options.fetch :add_reversed, false
-
146
@reload = @options.fetch :reload, false
-
146
@raw = false
-
-
146
merge_conditions([ DataMapper::Ext::Hash.except(@options, *OPTIONS), @options[:conditions] ])
-
146
normalize_options
-
end
-
-
# Copying contructor, called for Query#dup
-
#
-
# @api semipublic
-
1
def initialize_copy(*)
-
120
@fields = @fields.dup
-
120
@links = @links.dup
-
120
@conditions = @conditions.dup
-
120
@order = DataMapper::Ext.try_dup(@order)
-
end
-
-
# Validate the options
-
#
-
# @param [#each] options
-
# the options to validate
-
#
-
# @raise [ArgumentError]
-
# if any pairs in +options+ are invalid options
-
#
-
# @api private
-
1
def assert_valid_options(options)
-
377
options = options.to_hash
-
-
377
options.each do |attribute, value|
-
659
case attribute
-
91
when :fields then assert_valid_fields(value, options[:unique])
-
12
when :links then assert_valid_links(value)
-
18
when :conditions then assert_valid_conditions(value)
-
199
when :offset then assert_valid_offset(value, options[:limit])
-
199
when :limit then assert_valid_limit(value)
-
41
when :order then assert_valid_order(value, options[:fields])
-
when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
-
else
-
99
assert_valid_conditions(attribute => value)
-
end
-
end
-
end
-
-
# Verifies that value of :fields option
-
# refers to existing properties
-
#
-
# @api private
-
1
def assert_valid_fields(fields, unique)
-
91
fields = fields.to_ary
-
-
91
model = self.model
-
-
91
valid_properties = model.properties
-
-
91
model.descendants.each do |descendant|
-
valid_properties += descendant.properties
-
end
-
-
91
fields.each do |field|
-
97
case field
-
when Symbol, String
-
unless valid_properties.named?(field)
-
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
-
end
-
-
when Property
-
97
unless valid_properties.include?(field)
-
raise ArgumentError, "+options[:field]+ entry #{field.name.inspect} does not map to a property in #{model}"
-
end
-
-
else
-
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} of an unsupported object #{field.class}"
-
end
-
end
-
end
-
-
# Verifies that value of :links option
-
# refers to existing associations
-
#
-
# @api private
-
1
def assert_valid_links(links)
-
12
links = links.to_ary
-
-
12
if links.empty?
-
raise ArgumentError, '+options[:links]+ should not be empty'
-
end
-
-
12
links.each do |link|
-
24
case link
-
when Symbol, String
-
unless @relationships.named?(link.to_sym)
-
raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
-
end
-
-
when Associations::Relationship
-
# TODO: figure out how to validate links from other models
-
#unless @relationships.value?(link)
-
# raise ArgumentError, "+options[:links]+ entry #{link.name.inspect} does not map to a relationship in #{model}"
-
#end
-
-
else
-
raise ArgumentError, "+options[:links]+ entry #{link.inspect} of an unsupported object #{link.class}"
-
end
-
end
-
end
-
-
# Verifies that value of :conditions option
-
# refers to existing properties
-
#
-
# @api private
-
1
def assert_valid_conditions(conditions)
-
117
assert_kind_of 'options[:conditions]', conditions, Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array
-
-
117
case conditions
-
when Hash
-
117
conditions.each do |subject, bind_value|
-
117
case subject
-
when Symbol, ::String
-
70
original = subject
-
70
subject = subject.to_s
-
70
name = subject[0, subject.index('.') || subject.length]
-
-
70
unless @properties.named?(name) || @relationships.named?(name)
-
raise ArgumentError, "condition #{original.inspect} does not map to a property or relationship in #{model}"
-
end
-
-
when Property
-
31
unless @properties.include?(subject)
-
raise ArgumentError, "condition #{subject.name.inspect} does not map to a property in #{model}, but belongs to #{subject.model}"
-
end
-
-
when Operator
-
operator = subject.operator
-
-
unless Conditions::Comparison.slugs.include?(operator) || operator == :not
-
raise ArgumentError, "condition #{subject.inspect} used an invalid operator #{operator}"
-
end
-
-
assert_valid_conditions(subject.target => bind_value)
-
-
when Path
-
assert_valid_links(subject.relationships)
-
-
when Associations::Relationship
-
# TODO: validate that it belongs to the current model
-
#unless subject.source_model.equal?(model)
-
# raise ArgumentError, "condition #{subject.name.inspect} is not a valid relationship for #{model}, it's source model was #{subject.source_model}"
-
#end
-
-
else
-
raise ArgumentError, "condition #{subject.inspect} of an unsupported object #{subject.class}"
-
end
-
end
-
-
when Array
-
if conditions.empty?
-
raise ArgumentError, '+options[:conditions]+ should not be empty'
-
end
-
-
first_condition = conditions.first
-
-
unless first_condition.kind_of?(String) && !DataMapper::Ext.blank?(first_condition)
-
raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
-
end
-
end
-
end
-
-
# Verifies that query offset is non-negative and only used together with limit
-
# @api private
-
1
def assert_valid_offset(offset, limit)
-
199
offset = offset.to_int
-
-
199
unless offset >= 0
-
raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
-
end
-
-
199
if offset > 0 && limit.nil?
-
raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
-
end
-
end
-
-
# Verifies the limit is equal to or greater than 0
-
#
-
# @raise [ArgumentError]
-
# raised if the limit is not an Integer or less than 0
-
#
-
# @api private
-
1
def assert_valid_limit(limit)
-
199
limit = limit.to_int
-
-
199
unless limit >= 0
-
raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
-
end
-
end
-
-
# Verifies that :order option uses proper operator and refers
-
# to existing property
-
#
-
# @api private
-
1
def assert_valid_order(order, fields)
-
41
return if order.nil?
-
-
12
order = Array(order)
-
24
if order.empty? && fields && fields.any? { |property| !property.kind_of?(Operator) }
-
raise ArgumentError, '+options[:order]+ should not be empty if +options[:fields] contains a non-operator'
-
end
-
-
12
model = self.model
-
-
12
order.each do |order_entry|
-
case order_entry
-
when Symbol, String
-
unless @properties.named?(order_entry)
-
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
-
end
-
-
when Property, Path
-
# Allow any arbitrary property, since it may map to a model
-
# that has been included via the :links option
-
-
when Operator, Direction
-
operator = order_entry.operator
-
-
unless operator == :asc || operator == :desc
-
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} used an invalid operator #{operator}"
-
end
-
-
assert_valid_order([ order_entry.target ], fields)
-
-
else
-
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} of an unsupported object #{order_entry.class}"
-
end
-
end
-
end
-
-
# Used to verify value of boolean properties in conditions
-
# @api private
-
1
def assert_valid_boolean(name, value)
-
if value != true && value != false
-
raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
-
end
-
end
-
-
# Verifies that associations given in conditions belong
-
# to the same repository as query's model
-
#
-
# @api private
-
1
def assert_valid_other(other)
-
other_repository = other.repository
-
repository = self.repository
-
other_class = other.class
-
-
unless other_repository == repository
-
raise ArgumentError, "+other+ #{other_class} must be for the #{repository.name} repository, not #{other_repository.name}"
-
end
-
-
other_model = other.model
-
model = self.model
-
-
unless other_model >= model
-
raise ArgumentError, "+other+ #{other_class} must be for the #{model.name} model, not #{other_model.name}"
-
end
-
end
-
-
# Handle all the conditions options provided
-
#
-
# @param [Array<Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array>]
-
# a list of conditions
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def merge_conditions(conditions)
-
377
@conditions = Conditions::Operation.new(:and) << @conditions unless @conditions.nil?
-
-
377
conditions.compact!
-
377
conditions.each do |condition|
-
387
case condition
-
when Conditions::AbstractOperation, Conditions::AbstractComparison
-
add_condition(condition)
-
-
when Hash
-
496
condition.each { |key, value| append_condition(key, value) }
-
-
when Array
-
statement, *bind_values = *condition
-
raw_condition = [ statement ]
-
raw_condition << bind_values if bind_values.size > 0
-
add_condition(raw_condition)
-
@raw = true
-
end
-
end
-
end
-
-
# Normalize options
-
#
-
# @param [Array<Symbol>] options
-
# the options to normalize
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def normalize_options(options = OPTIONS)
-
377
normalize_order if options.include? :order
-
377
normalize_fields if options.include? :fields
-
377
normalize_links if options.include? :links
-
377
normalize_unique if options.include? :unique
-
end
-
-
# Normalize order elements to Query::Direction instances
-
#
-
# @api private
-
1
def normalize_order
-
187
return if @order.nil?
-
-
158
@order = Array(@order)
-
158
@order = @order.map do |order|
-
146
case order
-
when Direction
-
146
order.dup
-
-
when Operator
-
target = order.target
-
property = target.kind_of?(Property) ? target : @properties[target]
-
-
Direction.new(property, order.operator)
-
-
when Symbol, String
-
Direction.new(@properties[order])
-
-
when Property
-
Direction.new(order)
-
-
when Path
-
Direction.new(order.property)
-
-
end
-
end
-
end
-
-
# Normalize fields to Property instances
-
#
-
# @api private
-
1
def normalize_fields
-
235
@fields = @fields.map do |field|
-
798
case field
-
when Symbol, String
-
@properties[field]
-
-
when Property, Operator
-
798
field
-
end
-
end
-
end
-
-
# Normalize links to Query::Path
-
#
-
# Normalization means links given as symbols are replaced with
-
# relationships they refer to, intermediate links are "followed"
-
# and duplicates are removed
-
#
-
# @api private
-
1
def normalize_links
-
377
stack = @links.dup
-
-
377
@links.clear
-
-
377
while link = stack.pop
-
24
relationship = case link
-
when Symbol, String then @relationships[link]
-
24
when Associations::Relationship then link
-
end
-
-
24
if relationship.respond_to?(:links)
-
stack.concat(relationship.links)
-
24
elsif !@links.include?(relationship)
-
24
@links << relationship
-
end
-
end
-
-
377
@links.reverse!
-
end
-
-
# Normalize the unique attribute
-
#
-
# If any links are present, and the unique attribute was not
-
# explicitly specified, then make sure the query is marked as unique
-
#
-
# @api private
-
1
def normalize_unique
-
377
@unique = links.any? unless @options.key?(:unique)
-
end
-
-
# Append conditions to this Query
-
#
-
# TODO: needs example
-
#
-
# @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
-
# the subject to match
-
# @param [Object] bind_value
-
# the value to match on
-
# @param [Symbol] operator
-
# the operator to match with
-
#
-
# @return [Query::Conditions::AbstractOperation]
-
# the Query conditions
-
#
-
# @api private
-
1
def append_condition(subject, bind_value, model = self.model, operator = :eql)
-
249
case subject
-
109
when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
-
70
when Symbol then append_symbol_condition(subject, bind_value, model, operator)
-
70
when String then append_string_condition(subject, bind_value, model, operator)
-
when Operator then append_operator_conditions(subject, bind_value, model)
-
when Path then append_path(subject, bind_value, model, operator)
-
else
-
raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
-
end
-
end
-
-
# @api private
-
1
def equality_operator_for_type(bind_value)
-
109
case bind_value
-
70
when Model, String then :eql
-
when Enumerable then :in
-
when Regexp then :regexp
-
39
else :eql
-
end
-
end
-
-
# @api private
-
1
def append_property_condition(subject, bind_value, operator)
-
109
negated = operator == :not
-
-
109
if operator == :eql || negated
-
# transform :relationship => nil into :relationship.not => association
-
109
if subject.respond_to?(:collection_for) && bind_value.nil?
-
negated = !negated
-
bind_value = collection_for_nil(subject)
-
end
-
-
109
operator = equality_operator_for_type(bind_value)
-
end
-
-
109
condition = Conditions::Comparison.new(operator, subject, bind_value)
-
-
109
if negated
-
condition = Conditions::Operation.new(:not, condition)
-
end
-
-
109
add_condition(condition)
-
end
-
-
# @api private
-
1
def append_symbol_condition(symbol, bind_value, model, operator)
-
70
append_condition(symbol.to_s, bind_value, model, operator)
-
end
-
-
# @api private
-
1
def append_string_condition(string, bind_value, model, operator)
-
70
if string.include?('.')
-
query_path = model
-
-
target_components = string.split('.')
-
last_component = target_components.last
-
operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.any? { |slug| slug.to_s == last_component }
-
-
target_components.each { |method| query_path = query_path.send(method) }
-
-
append_condition(query_path, bind_value, model, operator)
-
else
-
70
repository_name = repository.name
-
70
subject = model.properties(repository_name)[string] ||
-
model.relationships(repository_name)[string]
-
-
70
append_condition(subject, bind_value, model, operator)
-
end
-
end
-
-
# @api private
-
1
def append_operator_conditions(operator, bind_value, model)
-
append_condition(operator.target, bind_value, model, operator.operator)
-
end
-
-
# @api private
-
1
def append_path(path, bind_value, model, operator)
-
path.relationships.each do |relationship|
-
inverse = relationship.inverse
-
@links.unshift(inverse) unless @links.include?(inverse)
-
end
-
-
append_condition(path.property, bind_value, path.model, operator)
-
end
-
-
# Add a condition to the Query
-
#
-
# @param [AbstractOperation, AbstractComparison]
-
# the condition to add to the Query
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def add_condition(condition)
-
109
@conditions = Conditions::Operation.new(:and) if @conditions.nil?
-
109
@conditions << condition
-
end
-
-
# Extract arguments for #slice and #slice! then return offset and limit
-
#
-
# @param [Integer, Array(Integer), Range] *args the offset,
-
# offset and limit, or range indicating first and last position
-
#
-
# @return [Integer] the offset
-
# @return [Integer, nil] the limit, if any
-
#
-
# @api private
-
1
def extract_slice_arguments(*args)
-
100
offset, limit = case args.size
-
100
when 2 then extract_offset_limit_from_two_arguments(*args)
-
when 1 then extract_offset_limit_from_one_argument(*args)
-
end
-
-
100
return offset, limit if offset && limit
-
-
raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
-
end
-
-
# @api private
-
1
def extract_offset_limit_from_two_arguments(*args)
-
300
args if args.all? { |arg| arg.kind_of?(Integer) }
-
end
-
-
# @api private
-
1
def extract_offset_limit_from_one_argument(arg)
-
case arg
-
when Integer then extract_offset_limit_from_integer(arg)
-
when Range then extract_offset_limit_from_range(arg)
-
end
-
end
-
-
# @api private
-
1
def extract_offset_limit_from_integer(integer)
-
[ integer, 1 ]
-
end
-
-
# @api private
-
1
def extract_offset_limit_from_range(range)
-
offset = range.first
-
limit = range.last - offset
-
limit = limit.succ unless range.exclude_end?
-
return offset, limit
-
end
-
-
# @api private
-
1
def get_relative_position(offset, limit)
-
self_offset = self.offset
-
self_limit = self.limit
-
new_offset = self_offset + offset
-
-
if limit <= 0 || (self_limit && new_offset + limit > self_offset + self_limit)
-
raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
-
end
-
-
return new_offset, limit
-
end
-
-
# TODO: DRY this up with conditions
-
# @api private
-
1
def record_value(record, property)
-
case record
-
when Hash
-
record.fetch(property, record[property.field])
-
when Resource
-
property.get!(record)
-
end
-
end
-
-
# @api private
-
1
def collection_for_nil(relationship)
-
query = relationship.query.dup
-
-
relationship.target_key.each do |target_key|
-
query[target_key.name.not] = nil if target_key.allow_nil?
-
end
-
-
relationship.target_model.all(query)
-
end
-
-
# @api private
-
1
def each_comparison
-
operands = conditions.operands.to_a
-
-
while operand = operands.shift
-
if operand.respond_to?(:operands)
-
operands.unshift(*operand.operands)
-
else
-
yield operand
-
end
-
end
-
end
-
-
# Apply a set operation on self and another query
-
#
-
# @param [Symbol] operation
-
# the set operation to apply
-
# @param [Query] other
-
# the other query to apply the set operation on
-
#
-
# @return [Query]
-
# the query that was created for the set operation
-
#
-
# @api private
-
1
def set_operation(operation, other)
-
assert_valid_other(other)
-
query = self.class.new(@repository, @model, other.to_relative_hash)
-
query.instance_variable_set(:@conditions, other_conditions(other, operation))
-
query
-
end
-
-
# Return the union with another query's conditions
-
#
-
# @param [Query] other
-
# the query conditions to union with
-
#
-
# @return [OrOperation]
-
# the union of the query conditions and other conditions
-
#
-
# @api private
-
1
def other_conditions(other, operation)
-
self_conditions = query_conditions(self)
-
-
unless self_conditions.kind_of?(Conditions::Operation)
-
operation_slug = case operation
-
when :intersection, :difference then :and
-
when :union then :or
-
end
-
-
self_conditions = Conditions::Operation.new(operation_slug, self_conditions)
-
end
-
-
self_conditions.send(operation, query_conditions(other))
-
end
-
-
# Extract conditions from a Query
-
#
-
# @param [Query] query
-
# the query with conditions
-
#
-
# @return [AbstractOperation]
-
# the operation
-
#
-
# @api private
-
1
def query_conditions(query)
-
if query.limit || query.links.any?
-
query.to_subquery
-
else
-
query.conditions
-
end
-
end
-
-
# Return a self referrential relationship
-
#
-
# @return [Associations::OneToMany::Relationship]
-
# the 1:m association to the same model
-
#
-
# @api private
-
1
def self_relationship
-
@self_relationship ||=
-
begin
-
model = self.model
-
Associations::OneToMany::Relationship.new(
-
:self,
-
model,
-
model,
-
self_relationship_options
-
)
-
end
-
end
-
-
# Return options for the self referrential relationship
-
#
-
# @return [Hash]
-
# the options to use with the self referrential relationship
-
#
-
# @api private
-
1
def self_relationship_options
-
keys = model_key.map { |property| property.name }
-
repository = self.repository
-
{
-
:child_key => keys,
-
:parent_key => keys,
-
:child_repository_name => repository.name,
-
:parent_repository_name => repository.name,
-
}
-
end
-
-
# Return the model key
-
#
-
# @return [PropertySet]
-
# the model key
-
#
-
# @api private
-
1
def model_key
-
@properties.key
-
end
-
end # class Query
-
end # module DataMapper
-
1
module DataMapper
-
1
class Query
-
# The Conditions module contains classes used as part of a Query when
-
# filtering collections of resources.
-
#
-
# The Conditions module contains two types of class used for filtering
-
# queries: Comparison and Operation. Although these are used on all
-
# repository types -- not just SQL-based repos -- these classes are best
-
# thought of as being the DataMapper counterpart to an SQL WHERE clause.
-
#
-
# Comparisons compare properties and relationships with values, while
-
# operations tie Comparisons together to form more complex expressions.
-
#
-
# For example, the following SQL query fragment:
-
#
-
# ... WHERE my_field = my_value AND another_field = another_value ...
-
#
-
# ... would be represented as two EqualToComparison instances tied
-
# together with an AndOperation.
-
#
-
# Conditions -- together with the Query class -- allow DataMapper to
-
# represent SQL-like expressions in an ORM-agnostic manner, and are used
-
# for both in-memory filtering of loaded Collection instances, and by
-
# adapters to retrieve records directly from your repositories.
-
#
-
# The classes contained in the Conditions module are for internal use by
-
# DataMapper and DataMapper plugins, and are not intended to be used
-
# directly in your applications.
-
1
module Conditions
-
-
# An abstract class which provides easy access to comparison operators
-
#
-
# @example Creating a new comparison
-
# Comparison.new(:eql, MyClass.my_property, "value")
-
#
-
1
class Comparison
-
-
# Creates a new Comparison instance
-
#
-
# The returned instance will be suitable for matching the given
-
# subject (property or relationship) against the value.
-
#
-
# @param [Symbol] slug
-
# The type of comparison operator required. One of: :eql, :in, :gt,
-
# :gte, :lt, :lte, :regexp, :like.
-
# @param [Property, Associations::Relationship]
-
# The subject of the comparison - the value of the subject will be
-
# matched against the given value parameter.
-
# @param [Object] value
-
# The value for the comparison.
-
#
-
# @return [DataMapper::Query::Conditions::AbstractComparison]
-
#
-
# @example
-
# Comparison.new(:eql, MyClass.properties[:id], 1)
-
#
-
# @api semipublic
-
1
def self.new(slug, subject, value)
-
109
if klass = comparison_class(slug)
-
109
klass.new(subject, value)
-
else
-
raise ArgumentError, "No Comparison class for #{slug.inspect} has been defined"
-
end
-
end
-
-
# Returns an array of all slugs registered with Comparison
-
#
-
# @return [Array<Symbol>]
-
#
-
# @api private
-
1
def self.slugs
-
18
AbstractComparison.descendants.map { |comparison_class| comparison_class.slug }
-
end
-
-
1
class << self
-
1
private
-
-
# Holds comparison subclasses keyed on their slug
-
#
-
# @return [Hash]
-
#
-
# @api private
-
1
def comparison_classes
-
109
@comparison_classes ||= {}
-
end
-
-
# Returns the comparison class identified by the given slug
-
#
-
# @param [Symbol] slug
-
# See slug parameter for Comparison.new
-
#
-
# @return [AbstractComparison, nil]
-
#
-
# @api private
-
1
def comparison_class(slug)
-
110
comparison_classes[slug] ||= AbstractComparison.descendants.detect { |comparison_class| comparison_class.slug == slug }
-
end
-
end
-
end # class Comparison
-
-
# A base class for the various comparison classes.
-
1
class AbstractComparison
-
1
extend Equalizer
-
-
1
equalize :subject, :value
-
-
# @api semipublic
-
1
attr_accessor :parent
-
-
# The property or relationship which is being matched against
-
#
-
# @return [Property, Associations::Relationship]
-
#
-
# @api semipublic
-
1
attr_reader :subject
-
-
# Value to be compared with the subject
-
#
-
# This value is compared against that contained in the subject when
-
# filtering collections, or the value in the repository when
-
# performing queries.
-
#
-
# In the case of primitive property, this is the value as it
-
# is stored in the repository.
-
#
-
# @return [Object]
-
#
-
# @api semipublic
-
1
def value
-
281
dumped_value
-
end
-
-
# The loaded/typecast value
-
#
-
# In the case of primitive types, this will be the same as +value+,
-
# however when using primitive property this stores the loaded value.
-
#
-
# If writing an adapter, you should use +value+, while plugin authors
-
# should refer to +loaded_value+.
-
#
-
#--
-
# As an example, you might use symbols with the Enum type in dm-types
-
#
-
# property :myprop, Enum[:open, :closed]
-
#
-
# These are stored in repositories as 1 and 2, respectively. +value+
-
# returns the 1 or 2, while +loaded_value+ returns the symbol.
-
#++
-
#
-
# @return [Object]
-
#
-
# @api semipublic
-
1
attr_reader :loaded_value
-
-
# Keeps track of AbstractComparison subclasses (used in Comparison)
-
#
-
# @return [Set<AbstractComparison>]
-
# @api private
-
1
def self.descendants
-
27
@descendants ||= DescendantSet.new
-
end
-
-
# Registers AbstractComparison subclasses (used in Comparison)
-
#
-
# @api private
-
1
def self.inherited(descendant)
-
8
descendants << descendant
-
end
-
-
# Setter/getter: allows subclasses to easily set their slug
-
#
-
# @param [Symbol] slug
-
# The slug to be set for this class. Passing nil returns the current
-
# value instead.
-
#
-
# @return [Symbol]
-
# The current slug set for the Comparison.
-
#
-
# @example Creating a MyComparison compairson with slug :exact.
-
# class MyComparison < AbstractComparison
-
# slug :exact
-
# end
-
#
-
# @api semipublic
-
1
def self.slug(slug = nil)
-
193
slug ? @slug = slug : @slug
-
end
-
-
# Return the comparison class slug
-
#
-
# @return [Symbol]
-
# the comparison class slug
-
#
-
# @api private
-
1
def slug
-
168
self.class.slug
-
end
-
-
# Test that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
match_property?(record)
-
end
-
-
# Tests that the Comparison is valid
-
#
-
# Subclasses can overload this to customise the means by which they
-
# determine the validity of the comparison. #valid? is called prior to
-
# performing a query on the repository: each Comparison within a Query
-
# must be valid otherwise the query will not be performed.
-
#
-
# @see DataMapper::Property#valid?
-
# @see DataMapper::Associations::Relationship#valid?
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def valid?
-
99
valid_for_subject?(loaded_value)
-
end
-
-
# Returns whether the subject is a Relationship
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def relationship?
-
false
-
end
-
-
# Returns whether the subject is a Property
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def property?
-
subject.kind_of?(Property)
-
end
-
-
# Returns a human-readable representation of this object
-
#
-
# @return [String]
-
#
-
# @api semipublic
-
1
def inspect
-
"#<#{self.class} @subject=#{@subject.inspect} " \
-
"@dumped_value=#{@dumped_value.inspect} @loaded_value=#{@loaded_value.inspect}>"
-
end
-
-
# Returns a string version of this Comparison object
-
#
-
# @example
-
# Comparison.new(:==, MyClass.my_property, "value")
-
# # => "my_property == value"
-
#
-
# @return [String]
-
#
-
# @api semipublic
-
1
def to_s
-
"#{subject.name} #{comparator_string} #{dumped_value.inspect}"
-
end
-
-
# @api private
-
1
def negated?
-
99
parent = self.parent
-
99
parent ? parent.negated? : false
-
end
-
-
1
private
-
-
# @api private
-
1
attr_reader :dumped_value
-
-
# Creates a new AbstractComparison instance with +subject+ and +value+
-
#
-
# @param [Property, Associations::Relationship] subject
-
# The subject of the comparison - the value of the subject will be
-
# matched against the given value parameter.
-
# @param [Object] value
-
# The value for the comparison.
-
#
-
# @api semipublic
-
1
def initialize(subject, value)
-
109
@subject = subject
-
109
@loaded_value = typecast(value)
-
109
@dumped_value = dump
-
end
-
-
# @api private
-
1
def match_property?(record, operator = :===)
-
expected.send(operator, record_value(record))
-
end
-
-
# Typecasts the given +val+ using subject#typecast
-
#
-
# If the subject has no typecast method the value is returned without
-
# any changes.
-
#
-
# @param [Object] val
-
# The object to attempt to typecast.
-
#
-
# @return [Object]
-
# The typecasted object.
-
#
-
# @see Property#typecast
-
#
-
# @api private
-
1
def typecast(value)
-
101
typecast_property(value)
-
end
-
-
# @api private
-
1
def typecast_property(value)
-
101
subject.typecast(value)
-
end
-
-
# Dumps the given loaded_value using subject#value
-
#
-
# This converts property values to the primitive as stored in the
-
# repository.
-
#
-
# @return [Object]
-
# The raw (dumped) object.
-
#
-
# @see Property#value
-
#
-
# @api private
-
1
def dump
-
101
dump_property(loaded_value)
-
end
-
-
# @api private
-
1
def dump_property(value)
-
101
subject.dump(value)
-
end
-
-
# Returns a value for the comparison +subject+
-
#
-
# Extracts value for the +subject+ property or relationship from the
-
# given +record+, where +record+ is a Resource instance or a Hash.
-
#
-
# @param [DataMapper::Resource, Hash] record
-
# The resource or hash from which to retrieve the value.
-
# @param [Property, Associations::Relationship]
-
# The subject of the comparison. For example, if this is a property,
-
# the value for the resources +subject+ property is retrieved.
-
# @param [Symbol] key_type
-
# In the event that +subject+ is a relationship, key_type indicated
-
# which key should be used to retrieve the value from the resource.
-
#
-
# @return [Object]
-
#
-
# @api semipublic
-
1
def record_value(record, key_type = :source_key)
-
subject = self.subject
-
case record
-
when Hash
-
record_value_from_hash(record, subject, key_type)
-
when Resource
-
record_value_from_resource(record, subject, key_type)
-
else
-
record
-
end
-
end
-
-
# Returns a value from a record hash
-
#
-
# Retrieves value for the +subject+ property or relationship from the
-
# given +hash+.
-
#
-
# @return [Object]
-
#
-
# @see AbstractComparison#record_value
-
#
-
# @api private
-
1
def record_value_from_hash(hash, subject, key_type)
-
hash.fetch subject, case subject
-
when Property
-
subject.load(hash[subject.field])
-
when Associations::Relationship
-
subject.send(key_type).map { |property|
-
record_value_from_hash(hash, property, key_type)
-
}
-
end
-
end
-
-
# Returns a value from a resource
-
#
-
# Extracts value for the +subject+ property or relationship from the
-
# given +resource+.
-
#
-
# @return [Object]
-
#
-
# @see AbstractComparison#record_value
-
#
-
# @api private
-
1
def record_value_from_resource(resource, subject, key_type)
-
case subject
-
when Property
-
subject.get!(resource)
-
when Associations::Relationship
-
subject.send(key_type).get!(resource)
-
end
-
end
-
-
# Retrieves the value of the +subject+
-
#
-
# @return [Object]
-
#
-
# @api semipublic
-
1
def expected(value = @loaded_value)
-
expected = record_value(value, :target_key)
-
-
if @subject.respond_to?(:source_key)
-
@subject.source_key.typecast(expected)
-
else
-
expected
-
end
-
end
-
-
# Test the value to see if it is valid
-
#
-
# @return [Boolean] true if the value is valid
-
#
-
# @api semipublic
-
1
def valid_for_subject?(loaded_value)
-
99
subject.valid?(loaded_value, negated?)
-
end
-
end # class AbstractComparison
-
-
# Included into comparisons which are capable of supporting
-
# Relationships.
-
1
module RelationshipHandler
-
# Returns whether this comparison subject is a Relationship
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def relationship?
-
304
subject.kind_of?(Associations::Relationship)
-
end
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
if relationship? && expected.respond_to?(:query)
-
match_relationship?(record)
-
else
-
super
-
end
-
end
-
-
# Returns the conditions required to match the subject relationship
-
#
-
# @return [Hash]
-
#
-
# @api semipublic
-
1
def foreign_key_mapping
-
relationship = subject.inverse
-
relationship = relationship.links.first if relationship.respond_to?(:links)
-
-
Query.target_conditions(value, relationship.source_key, relationship.target_key)
-
end
-
-
1
private
-
-
# @api private
-
1
def match_relationship?(record)
-
expected.query.conditions.matches?(record_value(record))
-
end
-
-
# Typecasts each value in the inclusion set
-
#
-
# @return [Array<Object>]
-
#
-
# @see AbtractComparison#typecast
-
#
-
# @api private
-
1
def typecast(value)
-
109
if relationship?
-
8
typecast_relationship(value)
-
else
-
101
super
-
end
-
end
-
-
# @api private
-
1
def dump
-
109
if relationship?
-
8
dump_relationship(loaded_value)
-
else
-
101
super
-
end
-
end
-
-
# @api private
-
1
def dump_relationship(value)
-
8
value
-
end
-
end # module RelationshipHandler
-
-
# Tests whether the value in the record is equal to the expected
-
# set for the Comparison.
-
1
class EqualToComparison < AbstractComparison
-
1
include RelationshipHandler
-
-
1
slug :eql
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
if expected.nil?
-
record_value(record).nil?
-
else
-
super
-
end
-
end
-
-
1
private
-
-
# @api private
-
1
def typecast_relationship(value)
-
8
case value
-
when Hash then typecast_hash(value)
-
8
when Resource then typecast_resource(value)
-
end
-
end
-
-
# @api private
-
1
def typecast_hash(hash)
-
subject = self.subject
-
subject.target_model.new(subject.query.merge(hash))
-
end
-
-
# @api private
-
1
def typecast_resource(resource)
-
8
resource
-
end
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'='
-
end
-
end # class EqualToComparison
-
-
# Tests whether the value in the record is contained in the
-
# expected set for the Comparison, where expected is an
-
# Array, Range, or Set.
-
1
class InclusionComparison < AbstractComparison
-
1
include RelationshipHandler
-
-
1
slug :in
-
-
# Checks that the Comparison is valid
-
#
-
# @see DataMapper::Query::Conditions::AbstractComparison#valid?
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def valid?
-
loaded_value = self.loaded_value
-
case loaded_value
-
when Collection then valid_collection?(loaded_value)
-
when Range then valid_range?(loaded_value)
-
when Enumerable then valid_enumerable?(loaded_value)
-
else
-
false
-
end
-
end
-
-
1
private
-
-
# @api private
-
1
def match_property?(record)
-
super(record, :include?)
-
end
-
-
# Overloads AbtractComparison#expected
-
#
-
# @return [Array<Object>]
-
# @see AbtractComparison#expected
-
#
-
# @api private
-
1
def expected
-
loaded_value = self.loaded_value
-
if loaded_value.kind_of?(Range)
-
typecast_range(loaded_value)
-
elsif loaded_value.respond_to?(:map)
-
# FIXME: causes a lazy load when a Collection
-
loaded_value.map { |val| super(val) }
-
else
-
super
-
end
-
end
-
-
# @api private
-
1
def valid_collection?(collection)
-
valid_for_subject?(collection)
-
end
-
-
# @api private
-
1
def valid_range?(range)
-
(range.any? || negated?) && valid_for_subject?(range.first) && valid_for_subject?(range.last)
-
end
-
-
# @api private
-
1
def valid_enumerable?(enumerable)
-
(!enumerable.empty? || negated?) && enumerable.all? { |entry| valid_for_subject?(entry) }
-
end
-
-
# @api private
-
1
def typecast_property(value)
-
if value.kind_of?(Range)
-
typecast_range(value)
-
elsif value.respond_to?(:map) && !value.kind_of?(String)
-
value.map { |entry| super(entry) }
-
else
-
super
-
end
-
end
-
-
# @api private
-
1
def typecast_range(range)
-
range.class.new(typecast_property(range.first), typecast_property(range.last), range.exclude_end?)
-
end
-
-
# @api private
-
1
def typecast_relationship(value)
-
case value
-
when Hash then typecast_hash(value)
-
when Resource then typecast_resource(value)
-
when Collection then typecast_collection(value)
-
when Enumerable then typecast_enumerable(value)
-
end
-
end
-
-
# @api private
-
1
def typecast_hash(hash)
-
subject = self.subject
-
subject.target_model.all(subject.query.merge(hash))
-
end
-
-
# @api private
-
1
def typecast_resource(resource)
-
resource.collection_for_self
-
end
-
-
# @api private
-
1
def typecast_collection(collection)
-
collection
-
end
-
-
# @api private
-
1
def typecast_enumerable(enumerable)
-
collection = nil
-
enumerable.each do |entry|
-
typecasted = typecast_relationship(entry)
-
if collection
-
collection |= typecasted
-
else
-
collection = typecasted
-
end
-
end
-
collection
-
end
-
-
# Dumps the given +val+ using subject#value
-
#
-
# @return [Array<Object>]
-
#
-
# @see AbtractComparison#dump
-
#
-
# @api private
-
1
def dump
-
loaded_value = self.loaded_value
-
if subject.respond_to?(:dump) && loaded_value.respond_to?(:map) && !loaded_value.kind_of?(Range)
-
dumped_value = loaded_value.map { |value| dump_property(value) }
-
dumped_value.uniq!
-
dumped_value
-
else
-
super
-
end
-
end
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'IN'
-
end
-
end # class InclusionComparison
-
-
# Tests whether the value in the record matches the expected
-
# regexp set for the Comparison.
-
1
class RegexpComparison < AbstractComparison
-
1
slug :regexp
-
-
# Checks that the Comparison is valid
-
#
-
# @see AbstractComparison#valid?
-
#
-
# @api semipublic
-
1
def valid?
-
loaded_value.kind_of?(Regexp)
-
end
-
-
1
private
-
-
# Returns the value untouched
-
#
-
# @return [Object]
-
#
-
# @api private
-
1
def typecast(value)
-
value
-
end
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'=~'
-
end
-
end # class RegexpComparison
-
-
# Tests whether the value in the record is like the expected set
-
# for the Comparison. Equivalent to a LIKE clause in an SQL database.
-
#
-
# TODO: move this to dm-more with DataObjectsAdapter plugins
-
1
class LikeComparison < AbstractComparison
-
1
slug :like
-
-
1
private
-
-
# Overloads the +expected+ method in AbstractComparison
-
#
-
# Return a regular expression suitable for matching against the
-
# records value.
-
#
-
# @return [Regexp]
-
#
-
# @see AbtractComparison#expected
-
#
-
# @api semipublic
-
1
def expected
-
Regexp.new('\A' << super.gsub('%', '.*').tr('_', '.') << '\z')
-
end
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'LIKE'
-
end
-
end # class LikeComparison
-
-
# Tests whether the value in the record is greater than the
-
# expected set for the Comparison.
-
1
class GreaterThanComparison < AbstractComparison
-
1
slug :gt
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
return false if expected.nil?
-
record_value = record_value(record)
-
!record_value.nil? && record_value > expected
-
end
-
-
1
private
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'>'
-
end
-
end # class GreaterThanComparison
-
-
# Tests whether the value in the record is less than the expected
-
# set for the Comparison.
-
1
class LessThanComparison < AbstractComparison
-
1
slug :lt
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
return false if expected.nil?
-
record_value = record_value(record)
-
!record_value.nil? && record_value < expected
-
end
-
-
1
private
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'<'
-
end
-
end # class LessThanComparison
-
-
# Tests whether the value in the record is greater than, or equal to,
-
# the expected set for the Comparison.
-
1
class GreaterThanOrEqualToComparison < AbstractComparison
-
1
slug :gte
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
return false if expected.nil?
-
record_value = record_value(record)
-
!record_value.nil? && record_value >= expected
-
end
-
-
1
private
-
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'>='
-
end
-
end # class GreaterThanOrEqualToComparison
-
-
# Tests whether the value in the record is less than, or equal to, the
-
# expected set for the Comparison.
-
1
class LessThanOrEqualToComparison < AbstractComparison
-
1
slug :lte
-
-
# Tests that the record value matches the comparison
-
#
-
# @param [Resource, Hash] record
-
# The record containing the value to be matched
-
#
-
# @return [Boolean]
-
#
-
# @api semipublic
-
1
def matches?(record)
-
return false if expected.nil?
-
record_value = record_value(record)
-
!record_value.nil? && record_value <= expected
-
end
-
-
1
private
-
-
# @return [String]
-
#
-
# @see AbstractComparison#to_s
-
#
-
# @api private
-
1
def comparator_string
-
'<='
-
end
-
end # class LessThanOrEqualToComparison
-
-
end # module Conditions
-
end # class Query
-
end # module DataMapper
-
1
module DataMapper
-
1
class Query
-
1
module Conditions
-
1
class Operation
-
# Factory method to initialize an operation
-
#
-
# @example
-
# operation = Operation.new(:and, comparison)
-
#
-
# @param [Symbol] slug
-
# the identifier for the operation class
-
# @param [Array] *operands
-
# the operands to initialize the operation with
-
#
-
# @return [AbstractOperation]
-
# the operation matching the slug
-
#
-
# @api semipublic
-
1
def self.new(slug, *operands)
-
263
if klass = operation_class(slug)
-
263
klass.new(*operands)
-
else
-
raise ArgumentError, "No Operation class for #{slug.inspect} has been defined"
-
end
-
end
-
-
# Return an Array of all the slugs for the operation classes
-
#
-
# @return [Array]
-
# the slugs of all the operation classes
-
#
-
# @api private
-
1
def self.slugs
-
AbstractOperation.descendants.map { |operation_class| operation_class.slug }
-
end
-
-
1
class << self
-
1
private
-
-
# Returns a Hash mapping the slugs to each class
-
#
-
# @return [Hash]
-
# Hash mapping the slug to the class
-
#
-
# @api private
-
1
def operation_classes
-
263
@operation_classes ||= {}
-
end
-
-
# Lookup the operation class based on the slug
-
#
-
# @example
-
# operation_class = Operation.operation_class(:and)
-
#
-
# @param [Symbol] slug
-
# the identifier for the operation class
-
#
-
# @return [Class]
-
# the operation class
-
#
-
# @api private
-
1
def operation_class(slug)
-
268
operation_classes[slug] ||= AbstractOperation.descendants.detect { |operation_class| operation_class.slug == slug }
-
end
-
end
-
end # class Operation
-
-
1
class AbstractOperation
-
1
include DataMapper::Assertions
-
1
include Enumerable
-
1
extend Equalizer
-
-
1
equalize :sorted_operands
-
-
# Returns the parent operation
-
#
-
# @return [AbstractOperation]
-
# the parent operation
-
#
-
# @api semipublic
-
1
attr_accessor :parent
-
-
# Returns the child operations and comparisons
-
#
-
# @return [Set<AbstractOperation, AbstractComparison, Array>]
-
# the set of operations and comparisons
-
#
-
# @api semipublic
-
1
attr_reader :operands
-
-
1
alias_method :children, :operands
-
-
# Returns the classes that inherit from AbstractComparison
-
#
-
# @return [Set]
-
# the descendant classes
-
#
-
# @api private
-
1
def self.descendants
-
9
@descendants ||= DescendantSet.new
-
end
-
-
# Hook executed when inheriting from AbstractComparison
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def self.inherited(descendant)
-
4
descendants << descendant
-
end
-
-
# Get and set the slug for the operation class
-
#
-
# @param [Symbol] slug
-
# optionally set the slug for the operation class
-
#
-
# @return [Symbol]
-
# the slug for the operation class
-
#
-
# @api semipublic
-
1
def self.slug(slug = nil)
-
108
slug ? @slug = slug : @slug
-
end
-
-
# Return the comparison class slug
-
#
-
# @return [Symbol]
-
# the comparison class slug
-
#
-
# @api private
-
1
def slug
-
99
self.class.slug
-
end
-
-
# Get the first operand
-
#
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# returns the first operand
-
#
-
# @api semipublic
-
1
def first
-
each { |operand| return operand }
-
nil
-
end
-
-
# Iterate through each operand in the operation
-
#
-
# @yield [operand]
-
# yields to each operand
-
#
-
# @yieldparam [AbstractOperation, AbstractComparison, Array] operand
-
# each operand
-
#
-
# @return [self]
-
# returns the operation
-
#
-
# @api semipublic
-
1
def each
-
701
@operands.each { |op| yield op }
-
301
self
-
end
-
-
# Test to see if there are operands
-
#
-
# @return [Boolean]
-
# returns true if there are operands
-
#
-
# @api semipublic
-
1
def empty?
-
@operands.empty?
-
end
-
-
# Test to see if there is one operand
-
#
-
# @return [Boolean]
-
# true if there is only one operand
-
#
-
# @api semipublic
-
1
def one?
-
@operands.size == 1
-
end
-
-
# Test if the operation is valid
-
#
-
# @return [Boolean]
-
# true if the operation is valid, false if not
-
#
-
# @api semipublic
-
1
def valid?
-
198
any? && all? { |op| valid_operand?(op) }
-
end
-
-
# Add an operand to the operation
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to add
-
#
-
# @return [self]
-
# the operation
-
#
-
# @api semipublic
-
1
def <<(operand)
-
117
assert_valid_operand_type(operand)
-
117
@operands << relate_operand(operand)
-
117
self
-
end
-
-
# Add operands to the operation
-
#
-
# @param [#each] operands
-
# the operands to add
-
#
-
# @return [self]
-
# the operation
-
#
-
# @api semipublic
-
1
def merge(operands)
-
133
operands.each { |op| self << op }
-
125
self
-
end
-
-
# Return the union with another operand
-
#
-
# @param [AbstractOperation] other
-
# the operand to union with
-
#
-
# @return [OrOperation]
-
# the union of the operation and operand
-
#
-
# @api semipublic
-
1
def union(other)
-
Operation.new(:or, dup, other.dup).minimize
-
end
-
-
1
alias_method :|, :union
-
1
alias_method :+, :union
-
-
# Return the intersection of the operation and another operand
-
#
-
# @param [AbstractOperation] other
-
# the operand to intersect with
-
#
-
# @return [AndOperation]
-
# the intersection of the operation and operand
-
#
-
# @api semipublic
-
1
def intersection(other)
-
Operation.new(:and, dup, other.dup).minimize
-
end
-
-
1
alias_method :&, :intersection
-
-
# Return the difference of the operation and another operand
-
#
-
# @param [AbstractOperation] other
-
# the operand to not match
-
#
-
# @return [AndOperation]
-
# the intersection of the operation and operand
-
#
-
# @api semipublic
-
1
def difference(other)
-
Operation.new(:and, dup, Operation.new(:not, other.dup)).minimize
-
end
-
-
1
alias_method :-, :difference
-
-
# Minimize the operation
-
#
-
# @return [self]
-
# the minimized operation
-
#
-
# @api semipublic
-
1
def minimize
-
self
-
end
-
-
# Clear the operands
-
#
-
# @return [self]
-
# the operation
-
#
-
# @api semipublic
-
1
def clear
-
@operands.clear
-
self
-
end
-
-
# Return the string representation of the operation
-
#
-
# @return [String]
-
# the string representation of the operation
-
#
-
# @api semipublic
-
1
def to_s
-
empty? ? '' : "(#{sort_by { |op| op.to_s }.map { |op| op.to_s }.join(" #{slug.to_s.upcase} ")})"
-
end
-
-
# Test if the operation is negated
-
#
-
# Defaults to return false.
-
#
-
# @return [Boolean]
-
# true if the operation is negated, false if not
-
#
-
# @api private
-
1
def negated?
-
99
parent = self.parent
-
99
parent ? parent.negated? : false
-
end
-
-
# Return a list of operands in predictable order
-
#
-
# @return [Array<AbstractOperation, AbstractComparison, Array>]
-
# list of operands sorted in deterministic order
-
#
-
# @api private
-
1
def sorted_operands
-
sort_by { |op| op.hash }
-
end
-
-
1
private
-
-
# Initialize an operation
-
#
-
# @param [Array<AbstractOperation, AbstractComparison, Array>] *operands
-
# the operands to include in the operation
-
#
-
# @return [AbstractOperation]
-
# the operation
-
#
-
# @api semipublic
-
1
def initialize(*operands)
-
117
@operands = Set.new
-
117
merge(operands)
-
end
-
-
# Copy an operation
-
#
-
# @param [AbstractOperation] original
-
# the original operation
-
#
-
# @return [undefined]
-
#
-
# @api semipublic
-
1
def initialize_copy(*)
-
120
@operands = map { |op| op.dup }.to_set
-
end
-
-
# Minimize the operands recursively
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def minimize_operands
-
# FIXME: why does Set#map! not work here?
-
@operands = map do |op|
-
relate_operand(op.respond_to?(:minimize) ? op.minimize : op)
-
end.to_set
-
end
-
-
# Prune empty operands recursively
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def prune_operands
-
@operands.delete_if { |op| op.respond_to?(:empty?) ? op.empty? : false }
-
end
-
-
# Test if the operand is valid
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to test
-
#
-
# @return [Boolean]
-
# true if the operand is valid
-
#
-
# @api private
-
1
def valid_operand?(operand)
-
99
if operand.respond_to?(:valid?)
-
99
operand.valid?
-
else
-
true
-
end
-
end
-
-
# Set self to be the operand's parent
-
#
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# the operand that was related to self
-
#
-
# @api private
-
1
def relate_operand(operand)
-
117
operand.parent = self if operand.respond_to?(:parent=)
-
117
operand
-
end
-
-
# Assert that the operand is a valid type
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to test
-
#
-
# @return [undefined]
-
#
-
# @raise [ArgumentError]
-
# raised if the operand is not a valid type
-
#
-
# @api private
-
1
def assert_valid_operand_type(operand)
-
117
assert_kind_of 'operand', operand, AbstractOperation, AbstractComparison, Array
-
end
-
end # class AbstractOperation
-
-
1
module FlattenOperation
-
# Add an operand to the operation, flattening the same types
-
#
-
# Flattening means that if the operand is the same as the
-
# operation, we should just include the operand's operands
-
# in the operation and prune that part of the tree. This results
-
# in a shallower tree, is faster to match and usually generates
-
# more efficient queries in the adapters.
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to add
-
#
-
# @return [self]
-
# the operation
-
#
-
# @api semipublic
-
1
def <<(operand)
-
125
if kind_of?(operand.class)
-
8
merge(operand.operands)
-
else
-
117
super
-
end
-
end
-
end # module FlattenOperation
-
-
1
class AndOperation < AbstractOperation
-
1
include FlattenOperation
-
-
1
slug :and
-
-
# Match the record
-
#
-
# @example with a Hash
-
# operation.matches?({ :id => 1 }) # => true
-
#
-
# @example with a Resource
-
# operation.matches?(Blog::Article.new(:id => 1)) # => true
-
#
-
# @param [Resource, Hash] record
-
# the resource to match
-
#
-
# @return [true]
-
# true if the record matches, false if not
-
#
-
# @api semipublic
-
1
def matches?(record)
-
all? { |op| op.respond_to?(:matches?) ? op.matches?(record) : true }
-
end
-
-
# Minimize the operation
-
#
-
# @return [self]
-
# the minimized AndOperation
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# the minimized operation
-
#
-
# @api semipublic
-
1
def minimize
-
minimize_operands
-
-
return Operation.new(:null) if any? && all? { |op| op.nil? }
-
-
prune_operands
-
-
one? ? first : self
-
end
-
end # class AndOperation
-
-
1
class OrOperation < AbstractOperation
-
1
include FlattenOperation
-
-
1
slug :or
-
-
# Match the record
-
#
-
# @param [Resource, Hash] record
-
# the resource to match
-
#
-
# @return [true]
-
# true if the record matches, false if not
-
#
-
# @api semipublic
-
1
def matches?(record)
-
any? { |op| op.respond_to?(:matches?) ? op.matches?(record) : true }
-
end
-
-
# Test if the operation is valid
-
#
-
# An OrOperation is valid if one of it's operands is valid.
-
#
-
# @return [Boolean]
-
# true if the operation is valid, false if not
-
#
-
# @api semipublic
-
1
def valid?
-
any? { |op| valid_operand?(op) }
-
end
-
-
# Minimize the operation
-
#
-
# @return [self]
-
# the minimized OrOperation
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# the minimized operation
-
#
-
# @api semipublic
-
1
def minimize
-
minimize_operands
-
-
return Operation.new(:null) if any? { |op| op.nil? }
-
-
prune_operands
-
-
one? ? first : self
-
end
-
end # class OrOperation
-
-
1
class NotOperation < AbstractOperation
-
1
slug :not
-
-
# Match the record
-
#
-
# @param [Resource, Hash] record
-
# the resource to match
-
#
-
# @return [true]
-
# true if the record matches, false if not
-
#
-
# @api semipublic
-
1
def matches?(record)
-
operand = self.operand
-
operand.respond_to?(:matches?) ? !operand.matches?(record) : true
-
end
-
-
# Add an operand to the operation
-
#
-
# This will only allow a single operand to be added.
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to add
-
#
-
# @return [self]
-
# the operation
-
#
-
# @api semipublic
-
1
def <<(operand)
-
assert_one_operand(operand)
-
assert_no_self_reference(operand)
-
super
-
end
-
-
# Return the only operand in the operation
-
#
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# the operand
-
#
-
# @api semipublic
-
1
def operand
-
first
-
end
-
-
# Minimize the operation
-
#
-
# @return [self]
-
# the minimized NotOperation
-
# @return [AbstractOperation, AbstractComparison, Array]
-
# the minimized operation
-
#
-
# @api semipublic
-
1
def minimize
-
minimize_operands
-
prune_operands
-
-
# factor out double negatives if possible
-
operand = self.operand
-
one? && instance_of?(operand.class) ? operand.operand : self
-
end
-
-
# Return the string representation of the operation
-
#
-
# @return [String]
-
# the string representation of the operation
-
#
-
# @api semipublic
-
1
def to_s
-
empty? ? '' : "NOT(#{operand.to_s})"
-
end
-
-
# Test if the operation is negated
-
#
-
# Defaults to return false.
-
#
-
# @return [Boolean]
-
# true if the operation is negated, false if not
-
#
-
# @api private
-
1
def negated?
-
parent = self.parent
-
parent ? !parent.negated? : true
-
end
-
-
1
private
-
-
# Assert there is only one operand
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to test
-
#
-
# @return [undefined]
-
#
-
# @raise [ArgumentError]
-
# raised if the operand is not a valid type
-
#
-
# @api private
-
1
def assert_one_operand(operand)
-
unless empty? || self.operand == operand
-
raise ArgumentError, "#{self.class} cannot have more than one operand"
-
end
-
end
-
-
# Assert the operand is not equal to self
-
#
-
# @param [AbstractOperation, AbstractComparison, Array] operand
-
# the operand to test
-
#
-
# @return [undefined]
-
#
-
# @raise [ArgumentError]
-
# raised if object is appended to itself
-
#
-
# @api private
-
1
def assert_no_self_reference(operand)
-
if equal?(operand)
-
raise ArgumentError, 'cannot append operand to itself'
-
end
-
end
-
end # class NotOperation
-
-
1
class NullOperation < AbstractOperation
-
1
undef_method :<<
-
1
undef_method :merge
-
-
1
slug :null
-
-
# Match the record
-
#
-
# A NullOperation matches every record.
-
#
-
# @param [Resource, Hash] record
-
# the resource to match
-
#
-
# @return [true]
-
# every record matches
-
#
-
# @api semipublic
-
1
def matches?(record)
-
record.kind_of?(Hash) || record.kind_of?(Resource)
-
end
-
-
# Test validity of the operation
-
#
-
# A NullOperation is always valid.
-
#
-
# @return [true]
-
# always valid
-
#
-
# @api semipublic
-
1
def valid?
-
13
true
-
end
-
-
# Treat the operation the same as nil
-
#
-
# @return [true]
-
# should be treated as nil
-
#
-
# @api semipublic
-
1
def nil?
-
478
true
-
end
-
-
# Inspecting the operation should return the same as nil
-
#
-
# @return [String]
-
# return the string 'nil'
-
#
-
# @api semipublic
-
1
def inspect
-
'nil'
-
end
-
-
1
private
-
-
# Initialize a NullOperation
-
#
-
# @return [NullOperation]
-
# the operation
-
#
-
# @api semipublic
-
1
def initialize
-
146
@operands = Set.new
-
end
-
end
-
end # module Conditions
-
end # class Query
-
end # module DataMapper
-
# TODO: rename this DM::Symbol::Direction
-
-
# TODO: add a method to convert it into a DM::Query::Sort object, eg:
-
# operator.sort_for(model)
-
-
# TODO: rename #target to #property_name
-
-
# TODO: make sure Query converts this into a DM::Query::Sort object
-
# immediately and passes that down to the Adapter
-
-
# TODO: remove #get method
-
-
1
module DataMapper
-
1
class Query
-
1
class Direction < Operator
-
-
# @api private
-
1
def reverse!
-
@operator = @operator == :asc ? :desc : :asc
-
self
-
end
-
-
# @api private
-
1
def get(resource)
-
Sort.new(target.get(resource), @operator == :asc)
-
end
-
-
1
private
-
-
# @api private
-
1
def initialize(target, operator = :asc)
-
3
super
-
end
-
end # class Direction
-
end # class Query
-
end # module DataMapper
-
# TODO: rename this DM::Symbol::Operator
-
-
# TODO: add a method to convert it into a DM::Query::AbstractComparison object, eg:
-
# operator.comparison_for(repository, model)
-
-
# TODO: rename #target to #property_name
-
-
1
module DataMapper
-
1
class Query
-
1
class Operator
-
1
include DataMapper::Assertions
-
1
extend Equalizer
-
-
1
equalize :target, :operator
-
-
# @api private
-
1
attr_reader :target
-
-
# @api private
-
1
attr_reader :operator
-
-
# @api private
-
1
def inspect
-
"#<#{self.class.name} @target=#{target.inspect} @operator=#{operator.inspect}>"
-
end
-
-
1
private
-
-
# @api private
-
1
def initialize(target, operator)
-
15
@target, @operator = target, operator.to_sym
-
end
-
end # class Operator
-
end # class Query
-
end # module DataMapper
-
# TODO: instead of an Array of Path objects, create a Relationship
-
# on the fly using :through on the previous relationship, creating a
-
# chain. Query::Path could then be a thin wrapper that specifies extra
-
# conditions on the Relationships, like the target property o match
-
# on.
-
-
1
module DataMapper
-
1
class Query
-
1
class Path
-
# TODO: replace this with BasicObject
-
1
instance_methods.each do |method|
-
next if method =~ /\A__/ ||
-
61
%w[ send class dup object_id kind_of? instance_of? respond_to? respond_to_missing? equal? freeze frozen? should should_not instance_variables instance_variable_set instance_variable_get instance_variable_defined? remove_instance_variable extend hash inspect to_s copy_object initialize_dup ].include?(method.to_s)
-
40
undef_method method
-
end
-
-
1
include DataMapper::Assertions
-
1
extend Equalizer
-
-
1
equalize :relationships, :property
-
-
# @api semipublic
-
1
attr_reader :repository_name
-
-
# @api semipublic
-
1
attr_reader :relationships
-
-
# @api semipublic
-
1
attr_reader :model
-
-
# @api semipublic
-
1
attr_reader :property
-
-
1
(Conditions::Comparison.slugs | [ :not ]).each do |slug|
-
9
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{slug} # def eql
-
#{"raise \"explicit use of '#{slug}' operator is deprecated (#{caller.first})\"" if slug == :eql || slug == :in} # raise "explicit use of 'eql' operator is deprecated (#{caller.first})"
-
Operator.new(self, #{slug.inspect}) # Operator.new(self, :eql)
-
end # end
-
RUBY
-
end
-
-
# @api public
-
1
def kind_of?(klass)
-
super || (defined?(@property) ? @property.kind_of?(klass) : false)
-
end
-
-
# @api public
-
1
def instance_of?(klass)
-
super || (defined?(@property) ? @property.instance_of?(klass) : false)
-
end
-
-
# Used for creating :order options. This technique may be deprecated,
-
# so marking as semipublic until the issue is resolved.
-
#
-
# @api semipublic
-
1
def asc
-
Operator.new(property, :asc)
-
end
-
-
# Used for creating :order options. This technique may be deprecated,
-
# so marking as semipublic until the issue is resolved.
-
#
-
# @api semipublic
-
1
def desc
-
Operator.new(property, :desc)
-
end
-
-
# @api semipublic
-
1
def respond_to?(method, include_private = false)
-
super ||
-
(defined?(@property) && @property.respond_to?(method, include_private)) ||
-
@model.relationships(@repository_name).named?(method) ||
-
@model.properties(@repository_name).named?(method)
-
end
-
-
1
private
-
-
# @api semipublic
-
1
def initialize(relationships, property_name = nil)
-
@relationships = relationships.to_ary.dup
-
-
last_relationship = @relationships.last
-
@repository_name = last_relationship.relative_target_repository_name
-
@model = last_relationship.target_model
-
-
if property_name
-
property_name = property_name.to_sym
-
@property = @model.properties(@repository_name)[property_name] ||
-
raise(ArgumentError, "Unknown property '#{property_name}' in #{@model}")
-
end
-
end
-
-
# @api semipublic
-
1
def method_missing(method, *args)
-
if @property
-
return @property.send(method, *args)
-
end
-
-
path_class = self.class
-
-
if relationship = @model.relationships(@repository_name)[method]
-
return path_class.new(@relationships.dup << relationship)
-
end
-
-
if @model.properties(@repository_name).named?(method)
-
return path_class.new(@relationships, method)
-
end
-
-
raise NoMethodError, "undefined property or relationship '#{method}' on #{@model}"
-
end
-
end # class Path
-
end # class Query
-
end # module DataMapper
-
# TODO: add #reverse and #reverse! methods
-
-
1
module DataMapper
-
1
class Query
-
1
class Sort
-
# @api semipublic
-
1
attr_reader :value
-
-
# @api semipublic
-
1
def direction
-
@ascending ? :ascending : :descending
-
end
-
-
# @api private
-
1
def <=>(other)
-
other_value = other.value
-
value_nil = @value.nil?
-
other_nil = other_value.nil?
-
-
cmp = case
-
when value_nil then other_nil ? 0 : 1
-
when other_nil then -1
-
else
-
@value <=> other_value
-
end
-
-
@ascending ? cmp : cmp * -1
-
end
-
-
1
private
-
-
# @api private
-
1
def initialize(value, ascending = true)
-
@value = value
-
@ascending = ascending
-
end
-
end # class Sort
-
end # class Query
-
end # module DataMapper
-
1
module DataMapper
-
-
# A {SubjectSet} that keeps track of relationships defined in a {Model}
-
#
-
1
class RelationshipSet < SubjectSet
-
-
# A list of all relationships in this set
-
#
-
# @deprecated use DataMapper::RelationshipSet#each or DataMapper::RelationshipSet#to_a instead
-
#
-
# @return [Array]
-
# a list of all relationships in the set
-
#
-
# @api semipublic
-
1
def values
-
warn "#{self.class}#values is deprecated. Use #{self.class}#each or #{self.class}#to_a instead: #{caller.first}"
-
to_a
-
end
-
-
# A list of all relationships in this set
-
#
-
# @deprecated use DataMapper::RelationshipSet#each instead
-
#
-
# @yield [DataMapper::Associations::Relationship]
-
# all relationships in the set
-
#
-
# @yieldparam [DataMapper::Associations::Relationship] relationship
-
# a relationship in the set
-
#
-
# @return [RelationshipSet] self
-
#
-
# @api semipublic
-
1
def each_value
-
warn "#{self.class}#each_value is deprecated. Use #{self.class}#each instead: #{caller.first}"
-
each { |relationship| yield(relationship) }
-
self
-
end
-
-
# Check wether this RelationshipSet includes an entry with the given name
-
#
-
# @deprecated use DataMapper::RelationshipSet#named? instead
-
#
-
# @param [#to_s] name
-
# the name of the entry to look for
-
#
-
# @return [Boolean]
-
# true if the set contains a relationship with the given name
-
#
-
# @api semipublic
-
1
def key?(name)
-
warn "#{self.class}#key? is deprecated. Use #{self.class}#named? instead: #{caller.first}"
-
named?(name)
-
end
-
-
# Check wether this RelationshipSet includes an entry with the given name
-
#
-
# @deprecated use DataMapper::RelationshipSet#named? instead
-
#
-
# @param [#to_s] name
-
# the name of the entry to look for
-
#
-
# @return [Boolean]
-
# true if the set contains a relationship with the given name
-
#
-
# @api semipublic
-
1
def has_key?(name)
-
warn "#{self.class}#has_key? is deprecated. Use #{self.class}#named? instead: #{caller.first}"
-
named?(name)
-
end
-
-
end # class RelationshipSet
-
end # module DataMapper
-
1
module DataMapper
-
1
class Repository
-
1
include DataMapper::Assertions
-
1
extend Equalizer
-
-
1
equalize :name
-
-
# Get the list of adapters registered for all Repositories,
-
# keyed by repository name.
-
#
-
# TODO: create example
-
#
-
# @return [Hash(Symbol => Adapters::AbstractAdapter)]
-
# the adapters registered for all Repositories
-
#
-
# @api private
-
1
def self.adapters
-
210
@adapters ||= {}
-
end
-
-
# Get the stack of current repository contexts
-
#
-
# TODO: create example
-
#
-
# @return [Array]
-
# List of Repository contexts for the current Thread
-
#
-
# @api private
-
1
def self.context
-
2402
Thread.current[:dm_repository_contexts] ||= []
-
end
-
-
# Get the default name of this Repository
-
#
-
# TODO: create example
-
#
-
# @return [Symbol]
-
# the default name of this repository
-
#
-
# @api private
-
1
def self.default_name
-
2985
:default
-
end
-
-
# @api semipublic
-
1
attr_reader :name
-
-
# @api semipublic
-
1
alias_method :to_sym, :name
-
-
# Get the adapter for this repository
-
#
-
# Lazy loads adapter setup from registered adapters
-
#
-
# TODO: create example
-
#
-
# @return [Adapters::AbstractAdapter]
-
# the adapter for this repository
-
#
-
# @raise [RepositoryNotSetupError]
-
# if there is no adapter registered for a repository named @name
-
#
-
# @api semipublic
-
1
def adapter
-
# Make adapter instantiation lazy so we can defer repository setup until it's actually
-
# needed. Do not remove this code.
-
@adapter ||=
-
begin
-
209
adapters = self.class.adapters
-
-
209
unless adapters.key?(@name)
-
raise RepositoryNotSetupError, "Adapter not set: #{@name}. Did you forget to setup?"
-
end
-
-
209
adapters[@name]
-
306
end
-
end
-
-
# Get the identity for a particular model within this repository.
-
#
-
# If one doesn't yet exist, create a new default in-memory IdentityMap
-
# for the requested model.
-
#
-
# TODO: create example
-
#
-
# @param [Model] model
-
# Model whose identity map should be returned
-
#
-
# @return [IdentityMap]
-
# The IdentityMap for model in this Repository
-
#
-
# @api private
-
1
def identity_map(model)
-
83
@identity_maps[model.base_model] ||= IdentityMap.new
-
end
-
-
# Executes a block in the scope of this Repository
-
#
-
# TODO: create example
-
#
-
# @yieldparam [Repository] repository
-
# yields self within the block
-
#
-
# @yield
-
# execute block in the scope of this Repository
-
#
-
# @api private
-
1
def scope
-
87
context = Repository.context
-
-
87
context << self
-
-
87
begin
-
87
yield self
-
ensure
-
87
context.pop
-
end
-
end
-
-
# Create a Query or subclass instance for this repository.
-
#
-
# @param [Model] model
-
# the Model to retrieve results from
-
# @param [Hash] options
-
# the conditions and scope
-
#
-
# @return [Query]
-
#
-
# @api semipublic
-
1
def new_query(model, options = {})
-
146
adapter.new_query(self, model, options)
-
end
-
-
# Create one or more resource instances in this repository.
-
#
-
# TODO: create example
-
#
-
# @param [Enumerable(Resource)] resources
-
# The list of resources (model instances) to create
-
#
-
# @return [Integer]
-
# The number of records that were actually saved into the data-store
-
#
-
# @api semipublic
-
1
def create(resources)
-
20
adapter.create(resources)
-
end
-
-
# Retrieve a collection of results of a query
-
#
-
# TODO: create example
-
#
-
# @param [Query] query
-
# composition of the query to perform
-
#
-
# @return [Array]
-
# result set of the query
-
#
-
# @api semipublic
-
1
def read(query)
-
100
return [] unless query.valid?
-
83
query.model.load(adapter.read(query), query)
-
end
-
-
# Update the attributes of one or more resource instances
-
#
-
# TODO: create example
-
#
-
# @param [Hash(Property => Object)] attributes
-
# hash of attribute values to set, keyed by Property
-
# @param [Collection] collection
-
# collection of records to be updated
-
#
-
# @return [Integer]
-
# the number of records updated
-
#
-
# @api semipublic
-
1
def update(attributes, collection)
-
return 0 unless collection.query.valid? && attributes.any?
-
adapter.update(attributes, collection)
-
end
-
-
# Delete one or more resource instances
-
#
-
# TODO: create example
-
#
-
# @param [Collection] collection
-
# collection of records to be deleted
-
#
-
# @return [Integer]
-
# the number of records deleted
-
#
-
# @api semipublic
-
1
def delete(collection)
-
return 0 unless collection.query.valid?
-
adapter.delete(collection)
-
end
-
-
# Return a human readable representation of the repository
-
#
-
# TODO: create example
-
#
-
# @return [String]
-
# human readable representation of the repository
-
#
-
# @api private
-
1
def inspect
-
"#<#{self.class.name} @name=#{@name}>"
-
end
-
-
1
private
-
-
# Initializes a new Repository
-
#
-
# TODO: create example
-
#
-
# @param [Symbol] name
-
# The name of the Repository
-
#
-
# @api semipublic
-
1
def initialize(name)
-
1000
@name = name.to_sym
-
1000
@identity_maps = {}
-
end
-
end # class Repository
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
include DataMapper::Assertions
-
-
# @deprecated
-
1
def self.append_inclusions(*inclusions)
-
raise "DataMapper::Resource.append_inclusions is deprecated, use DataMapper::Model.append_inclusions instead (#{caller.first})"
-
end
-
-
# @deprecated
-
1
def self.extra_inclusions
-
raise "DataMapper::Resource.extra_inclusions is deprecated, use DataMapper::Model.extra_inclusions instead (#{caller.first})"
-
end
-
-
# @deprecated
-
1
def self.descendants
-
raise "DataMapper::Resource.descendants is deprecated, use DataMapper::Model.descendants instead (#{caller.first})"
-
end
-
-
# Return if Resource#save should raise an exception on save failures (per-resource)
-
#
-
# This delegates to model.raise_on_save_failure by default.
-
#
-
# user.raise_on_save_failure # => false
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def raise_on_save_failure
-
24
if defined?(@raise_on_save_failure)
-
@raise_on_save_failure
-
else
-
24
model.raise_on_save_failure
-
end
-
end
-
-
# Specify if Resource#save should raise an exception on save failures (per-resource)
-
#
-
# @param [Boolean]
-
# a boolean that if true will cause Resource#save to raise an exception
-
#
-
# @return [Boolean]
-
# true if a failure in Resource#save should raise an exception
-
#
-
# @api public
-
1
def raise_on_save_failure=(raise_on_save_failure)
-
@raise_on_save_failure = raise_on_save_failure
-
end
-
-
# Deprecated API for updating attributes and saving Resource
-
#
-
# @see #update
-
#
-
# @deprecated
-
1
def update_attributes(attributes = {}, *allowed)
-
raise "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller.first})"
-
end
-
-
# Makes sure a class gets all the methods when it includes Resource
-
#
-
# Note that including this module into an anonymous class will leave
-
# the model descendant tracking mechanism with no possibility to reliably
-
# track the anonymous model across code reloads. This means that
-
# {DataMapper::DescendantSet} will currently leak memory in scenarios where
-
# anonymous models are reloaded multiple times (as is the case in dm-rails
-
# development mode for example).
-
#
-
# @api private
-
1
def self.included(model)
-
5
model.extend Model
-
end
-
-
# @api public
-
1
alias_method :model, :class
-
-
# Get the persisted state for the resource
-
#
-
# @return [Resource::PersistenceState]
-
# the current persisted state for the resource
-
#
-
# @api private
-
1
def persistence_state
-
821
@_persistence_state ||= Resource::PersistenceState::Transient.new(self)
-
end
-
-
# Set the persisted state for the resource
-
#
-
# @param [Resource::PersistenceState]
-
# the new persisted state for the resource
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def persistence_state=(state)
-
158
@_persistence_state = state
-
end
-
-
# Test if the persisted state is set
-
#
-
# @return [Boolean]
-
# true if the persisted state is set
-
#
-
# @api private
-
1
def persistence_state?
-
34
defined?(@_persistence_state) ? true : false
-
end
-
-
# Repository this resource belongs to in the context of this collection
-
# or of the resource's class.
-
#
-
# @return [Repository]
-
# the respository this resource belongs to, in the context of
-
# a collection OR in the instance's Model's context
-
#
-
# @api semipublic
-
1
def repository
-
# only set @_repository explicitly when persisted
-
859
defined?(@_repository) ? @_repository : model.repository
-
end
-
-
# Retrieve the key(s) for this resource.
-
#
-
# This always returns the persisted key value,
-
# even if the key is changed and not yet persisted.
-
# This is done so all relations still work.
-
#
-
# @return [Array(Key)]
-
# the key(s) identifying this resource
-
#
-
# @api public
-
1
def key
-
94
return @_key if defined?(@_key)
-
-
53
model_key = model.key(repository_name)
-
-
53
key = model_key.map do |property|
-
53
original_attributes[property] || (property.loaded?(self) ? property.get!(self) : nil)
-
end
-
-
# only memoize a valid key
-
53
@_key = key if model_key.valid?(key)
-
end
-
-
# Checks if this Resource instance is new
-
#
-
# @return [Boolean]
-
# true if the resource is new and not saved
-
#
-
# @api public
-
1
def new?
-
90
persistence_state.kind_of?(PersistenceState::Transient)
-
end
-
-
# Checks if this Resource instance is saved
-
#
-
# @return [Boolean]
-
# true if the resource has been saved
-
#
-
# @api public
-
1
def saved?
-
81
persistence_state.kind_of?(PersistenceState::Persisted)
-
end
-
-
# Checks if this Resource instance is destroyed
-
#
-
# @return [Boolean]
-
# true if the resource has been destroyed
-
#
-
# @api public
-
1
def destroyed?
-
51
readonly? && !key.nil?
-
end
-
-
# Checks if the resource has no changes to save
-
#
-
# @return [Boolean]
-
# true if the resource may not be persisted
-
#
-
# @api public
-
1
def clean?
-
persistence_state.kind_of?(PersistenceState::Clean) ||
-
20
persistence_state.kind_of?(PersistenceState::Immutable)
-
end
-
-
# Checks if the resource has unsaved changes
-
#
-
# @return [Boolean]
-
# true if resource may be persisted
-
#
-
# @api public
-
1
def dirty?
-
20
run_once(true) do
-
20
dirty_self? || dirty_parents? || dirty_children?
-
end
-
end
-
-
# Checks if this Resource instance is readonly
-
#
-
# @return [Boolean]
-
# true if the resource cannot be persisted
-
#
-
# @api public
-
1
def readonly?
-
61
persistence_state.kind_of?(PersistenceState::Immutable)
-
end
-
-
# Returns the value of the attribute.
-
#
-
# Do not read from instance variables directly, but use this method.
-
# This method handles lazy loading the attribute and returning of
-
# defaults if nessesary.
-
#
-
# @example
-
# class Foo
-
# include DataMapper::Resource
-
#
-
# property :first_name, String
-
# property :last_name, String
-
#
-
# def full_name
-
# "#{attribute_get(:first_name)} #{attribute_get(:last_name)}"
-
# end
-
#
-
# # using the shorter syntax
-
# def name_for_address_book
-
# "#{last_name}, #{first_name}"
-
# end
-
# end
-
#
-
# @param [Symbol] name
-
# name of attribute to retrieve
-
#
-
# @return [Object]
-
# the value stored at that given attribute
-
# (nil if none, and default if necessary)
-
#
-
# @api public
-
1
def attribute_get(name)
-
property = properties[name]
-
persistence_state.get(property) if property
-
end
-
-
1
alias_method :[], :attribute_get
-
-
# Sets the value of the attribute and marks the attribute as dirty
-
# if it has been changed so that it may be saved. Do not set from
-
# instance variables directly, but use this method. This method
-
# handles the lazy loading the property and returning of defaults
-
# if nessesary.
-
#
-
# @example
-
# class Foo
-
# include DataMapper::Resource
-
#
-
# property :first_name, String
-
# property :last_name, String
-
#
-
# def full_name(name)
-
# name = name.split(' ')
-
# attribute_set(:first_name, name[0])
-
# attribute_set(:last_name, name[1])
-
# end
-
#
-
# # using the shorter syntax
-
# def name_from_address_book(name)
-
# name = name.split(', ')
-
# first_name = name[1]
-
# last_name = name[0]
-
# end
-
# end
-
#
-
# @param [Symbol] name
-
# name of attribute to set
-
# @param [Object] value
-
# value to store
-
#
-
# @return [undefined]
-
#
-
# @api public
-
1
def attribute_set(name, value)
-
property = properties[name]
-
if property
-
value = property.typecast(value)
-
self.persistence_state = persistence_state.set(property, value)
-
end
-
end
-
-
1
alias_method :[]=, :attribute_set
-
-
# Gets all the attributes of the Resource instance
-
#
-
# @param [Symbol] key_on
-
# Use this attribute of the Property as keys.
-
# defaults to :name. :field is useful for adapters
-
# :property or nil use the actual Property object.
-
#
-
# @return [Hash]
-
# All the attributes
-
#
-
# @api public
-
1
def attributes(key_on = :name)
-
attributes = {}
-
-
lazy_load(properties)
-
fields.each do |property|
-
if model.public_method_defined?(name = property.name)
-
key = case key_on
-
when :name then name
-
when :field then property.field
-
else property
-
end
-
-
attributes[key] = __send__(name)
-
end
-
end
-
-
attributes
-
end
-
-
# Assign values to multiple attributes in one call (mass assignment)
-
#
-
# @param [Hash] attributes
-
# names and values of attributes to assign
-
#
-
# @return [Hash]
-
# names and values of attributes assigned
-
#
-
# @api public
-
1
def attributes=(attributes)
-
36
model = self.model
-
36
attributes.each do |name, value|
-
104
case name
-
when String, Symbol
-
100
if model.allowed_writer_methods.include?(setter = "#{name}=")
-
100
__send__(setter, value)
-
else
-
raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
-
end
-
when Associations::Relationship, Property
-
# only call a public #typecast (e.g. on Property instances)
-
4
if name.respond_to?(:typecast)
-
value = name.typecast(value)
-
end
-
4
self.persistence_state = persistence_state.set(name, value)
-
end
-
end
-
end
-
-
# Reloads association and all child association
-
#
-
# This is accomplished by resetting the Resource key to it's
-
# original value, and then removing all the ivars for properties
-
# and relationships. On the next access of those ivars, the
-
# resource will eager load what it needs. While this is more of
-
# a lazy reload, it should result in more consistent behavior
-
# since no cached results will remain from the initial load.
-
#
-
# @return [Resource]
-
# the receiver, the current Resource instance
-
#
-
# @api public
-
1
def reload
-
if key
-
reset_key
-
clear_subjects
-
end
-
-
self.persistence_state = persistence_state.rollback
-
-
self
-
end
-
-
# Updates attributes and saves this Resource instance
-
#
-
# @param [Hash] attributes
-
# attributes to be updated
-
#
-
# @return [Boolean]
-
# true if resource and storage state match
-
#
-
# @api public
-
1
def update(attributes)
-
assert_update_clean_only(:update)
-
self.attributes = attributes
-
save
-
end
-
-
# Updates attributes and saves this Resource instance, bypassing hooks
-
#
-
# @param [Hash] attributes
-
# attributes to be updated
-
#
-
# @return [Boolean]
-
# true if resource and storage state match
-
#
-
# @api public
-
1
def update!(attributes)
-
assert_update_clean_only(:update!)
-
self.attributes = attributes
-
save!
-
end
-
-
# Save the instance and loaded, dirty associations to the data-store
-
#
-
# @return [Boolean]
-
# true if Resource instance and all associations were saved
-
#
-
# @api public
-
1
def save
-
51
assert_not_destroyed(:save)
-
51
retval = _save
-
51
assert_save_successful(:save, retval)
-
51
retval
-
end
-
-
# Save the instance and loaded, dirty associations to the data-store, bypassing hooks
-
#
-
# @return [Boolean]
-
# true if Resource instance and all associations were saved
-
#
-
# @api public
-
1
def save!
-
assert_not_destroyed(:save!)
-
retval = _save(false)
-
assert_save_successful(:save!, retval)
-
retval
-
end
-
-
# Destroy the instance, remove it from the repository
-
#
-
# @return [Boolean]
-
# true if resource was destroyed
-
#
-
# @api public
-
1
def destroy
-
return true if destroyed?
-
catch :halt do
-
before_destroy_hook
-
_destroy
-
after_destroy_hook
-
end
-
destroyed?
-
end
-
-
# Destroy the instance, remove it from the repository, bypassing hooks
-
#
-
# @return [Boolean]
-
# true if resource was destroyed
-
#
-
# @api public
-
1
def destroy!
-
return true if destroyed?
-
_destroy(false)
-
destroyed?
-
end
-
-
# Compares another Resource for equality
-
#
-
# Resource is equal to +other+ if they are the same object
-
# (identical object_id) or if they are both of the *same model* and
-
# all of their attributes are equivalent
-
#
-
# @param [Resource] other
-
# the other Resource to compare with
-
#
-
# @return [Boolean]
-
# true if they are equal, false if not
-
#
-
# @api public
-
1
def eql?(other)
-
return true if equal?(other)
-
instance_of?(other.class) && cmp?(other, :eql?)
-
end
-
-
# Compares another Resource for equivalency
-
#
-
# Resource is equivalent to +other+ if they are the same object
-
# (identical object_id) or all of their attribute are equivalent
-
#
-
# @param [Resource] other
-
# the other Resource to compare with
-
#
-
# @return [Boolean]
-
# true if they are equivalent, false if not
-
#
-
# @api public
-
1
def ==(other)
-
4
return true if equal?(other)
-
2
return false unless other.kind_of?(Resource) && model.base_model.equal?(other.model.base_model)
-
2
cmp?(other, :==)
-
end
-
-
# Compares two Resources to allow them to be sorted
-
#
-
# @param [Resource] other
-
# The other Resource to compare with
-
#
-
# @return [Integer]
-
# Return 0 if Resources should be sorted as the same, -1 if the
-
# other Resource should be after self, and 1 if the other Resource
-
# should be before self
-
#
-
# @api public
-
1
def <=>(other)
-
model = self.model
-
unless other.kind_of?(model.base_model)
-
raise ArgumentError, "Cannot compare a #{other.class} instance with a #{model} instance"
-
end
-
model.default_order(repository_name).each do |direction|
-
cmp = direction.get(self) <=> direction.get(other)
-
return cmp if cmp.nonzero?
-
end
-
0
-
end
-
-
# Returns hash value of the object.
-
# Two objects with the same hash value assumed equal (using eql? method)
-
#
-
# DataMapper resources are equal when their models have the same hash
-
# and they have the same set of properties
-
#
-
# When used as key in a Hash or Hash subclass, objects are compared
-
# by eql? and thus hash value has direct effect on lookup
-
#
-
# @api private
-
1
def hash
-
26
model.hash ^ key.hash
-
end
-
-
# Get a Human-readable representation of this Resource instance
-
#
-
# Foo.new #=> #<Foo name=nil updated_at=nil created_at=nil id=nil>
-
#
-
# @return [String]
-
# Human-readable representation of this Resource instance
-
#
-
# @api public
-
1
def inspect
-
# TODO: display relationship values
-
attrs = properties.map do |property|
-
value = if new? || property.loaded?(self)
-
property.get!(self).inspect
-
else
-
'<not loaded>'
-
end
-
-
"#{property.instance_variable_name}=#{value}"
-
end
-
-
"#<#{model.name} #{attrs.join(' ')}>"
-
end
-
-
# Hash of original values of attributes that have unsaved changes
-
#
-
# @return [Hash]
-
# original values of attributes that have unsaved changes
-
#
-
# @api semipublic
-
1
def original_attributes
-
121
if persistence_state.respond_to?(:original_attributes)
-
96
persistence_state.original_attributes.dup.freeze
-
else
-
25
{}.freeze
-
end
-
end
-
-
# Checks if an attribute has been loaded from the repository
-
#
-
# @example
-
# class Foo
-
# include DataMapper::Resource
-
#
-
# property :name, String
-
# property :description, Text, :lazy => false
-
# end
-
#
-
# Foo.new.attribute_loaded?(:description) #=> false
-
#
-
# @return [Boolean]
-
# true if ivar +name+ has been loaded
-
#
-
# @return [Boolean]
-
# true if ivar +name+ has been loaded
-
#
-
# @api private
-
1
def attribute_loaded?(name)
-
properties[name].loaded?(self)
-
end
-
-
# Checks if an attribute has unsaved changes
-
#
-
# @param [Symbol] name
-
# name of attribute to check for unsaved changes
-
#
-
# @return [Boolean]
-
# true if attribute has unsaved changes
-
#
-
# @api semipublic
-
1
def attribute_dirty?(name)
-
dirty_attributes.key?(properties[name])
-
end
-
-
# Hash of attributes that have unsaved changes
-
#
-
# @return [Hash]
-
# attributes that have unsaved changes
-
#
-
# @api semipublic
-
1
def dirty_attributes
-
20
dirty_attributes = {}
-
-
20
original_attributes.each_key do |property|
-
46
next unless property.respond_to?(:dump)
-
45
dirty_attributes[property] = property.dump(property.get!(self))
-
end
-
-
20
dirty_attributes
-
end
-
-
# Returns the Collection the Resource is associated with
-
#
-
# @return [nil]
-
# nil if this is a new record
-
# @return [Collection]
-
# a Collection that self belongs to
-
#
-
# @api private
-
1
def collection
-
2
return @_collection if @_collection || new? || readonly?
-
2
collection_for_self
-
end
-
-
# Associates a Resource to a Collection
-
#
-
# @param [Collection, nil] collection
-
# the collection to associate the resource with
-
#
-
# @return [nil]
-
# nil if this is a new record
-
# @return [Collection]
-
# a Collection that self belongs to
-
#
-
# @api private
-
1
def collection=(collection)
-
4
@_collection = collection
-
end
-
-
# Return a collection including the current resource only
-
#
-
# @return [Collection]
-
# a collection containing self
-
#
-
# @api private
-
1
def collection_for_self
-
2
Collection.new(query, [ self ])
-
end
-
-
# Returns a Query that will match the resource
-
#
-
# @return [Query]
-
# Query that will match the resource
-
#
-
# @api semipublic
-
1
def query
-
2
repository.new_query(model, :fields => fields, :conditions => conditions)
-
end
-
-
1
protected
-
-
# Method for hooking callbacks before resource saving
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def before_save_hook
-
20
execute_hooks_for(:before, :save)
-
end
-
-
# Method for hooking callbacks after resource saving
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def after_save_hook
-
20
execute_hooks_for(:after, :save)
-
end
-
-
# Method for hooking callbacks before resource creation
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def before_create_hook
-
20
execute_hooks_for(:before, :create)
-
end
-
-
# Method for hooking callbacks after resource creation
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def after_create_hook
-
20
execute_hooks_for(:after, :create)
-
end
-
-
# Method for hooking callbacks before resource updating
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def before_update_hook
-
execute_hooks_for(:before, :update)
-
end
-
-
# Method for hooking callbacks after resource updating
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def after_update_hook
-
execute_hooks_for(:after, :update)
-
end
-
-
# Method for hooking callbacks before resource destruction
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def before_destroy_hook
-
execute_hooks_for(:before, :destroy)
-
end
-
-
# Method for hooking callbacks after resource destruction
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def after_destroy_hook
-
execute_hooks_for(:after, :destroy)
-
end
-
-
1
private
-
-
# Initialize a new instance of this Resource using the provided values
-
#
-
# @param [Hash] attributes
-
# attribute values to use for the new instance
-
#
-
# @return [Hash]
-
# attribute values used in the new instance
-
#
-
# @api public
-
1
def initialize(attributes = nil) # :nodoc:
-
54
self.attributes = attributes if attributes
-
end
-
-
# @api private
-
1
def initialize_copy(original)
-
instance_variables.each do |ivar|
-
instance_variable_set(ivar, DataMapper::Ext.try_dup(instance_variable_get(ivar)))
-
end
-
-
self.persistence_state = persistence_state.class.new(self)
-
end
-
-
# Returns name of the repository this object
-
# was loaded from
-
#
-
# @return [String]
-
# name of the repository this object was loaded from
-
#
-
# @api private
-
1
def repository_name
-
370
repository.name
-
end
-
-
# Gets this instance's Model's properties
-
#
-
# @return [PropertySet]
-
# List of this Resource's Model's properties
-
#
-
# @api private
-
1
def properties
-
221
model.properties(repository_name)
-
end
-
-
# Gets this instance's Model's relationships
-
#
-
# @return [RelationshipSet]
-
# List of this instance's Model's Relationships
-
#
-
# @api private
-
1
def relationships
-
96
model.relationships(repository_name)
-
end
-
-
# Returns the identity map for the model from the repository
-
#
-
# @return [IdentityMap]
-
# identity map of repository this object was loaded from
-
#
-
# @api private
-
1
def identity_map
-
repository.identity_map(model)
-
end
-
-
# @api private
-
1
def add_to_identity_map
-
identity_map[key] = self
-
end
-
-
# @api private
-
1
def remove_from_identity_map
-
identity_map.delete(key)
-
end
-
-
# Fetches all the names of the attributes that have been loaded,
-
# even if they are lazy but have been called
-
#
-
# @return [Array<Property>]
-
# names of attributes that have been loaded
-
#
-
# @api private
-
1
def fields
-
2
properties.select do |property|
-
4
property.loaded?(self) || (new? && property.default?)
-
end
-
end
-
-
# Reset the key to the original value
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def reset_key
-
properties.key.zip(key) do |property, value|
-
property.set!(self, value)
-
end
-
end
-
-
# Remove all the ivars for properties and relationships
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def clear_subjects
-
model_properties = properties
-
-
(model_properties - model_properties.key | relationships).each do |subject|
-
next unless subject.loaded?(self)
-
remove_instance_variable(subject.instance_variable_name)
-
end
-
end
-
-
# Lazy loads attributes not yet loaded
-
#
-
# @param [Array<Property>] properties
-
# the properties to reload
-
#
-
# @return [self]
-
#
-
# @api private
-
1
def lazy_load(properties)
-
eager_load(properties - fields)
-
end
-
-
# Reloads specified attributes
-
#
-
# @param [Array<Property>] properties
-
# the properties to reload
-
#
-
# @return [Resource]
-
# the receiver, the current Resource instance
-
#
-
# @api private
-
1
def eager_load(properties)
-
8
unless properties.empty? || key.nil? || collection.nil?
-
# set an initial value to prevent recursive lazy loads
-
properties.each { |property| property.set!(self, nil) }
-
-
collection.reload(:fields => properties)
-
end
-
-
8
self
-
end
-
-
# Return conditions to match the Resource
-
#
-
# @return [Hash]
-
# query conditions
-
#
-
# @api private
-
1
def conditions
-
2
key = self.key
-
2
if key
-
2
model.key_conditions(repository, key)
-
else
-
conditions = {}
-
properties.each do |property|
-
next unless property.loaded?(self)
-
conditions[property] = property.get!(self)
-
end
-
conditions
-
end
-
end
-
-
# @api private
-
1
def parent_relationships
-
52
parent_relationships = []
-
-
52
relationships.each do |relationship|
-
113
next unless relationship.respond_to?(:resource_for)
-
9
set_default_value(relationship)
-
9
next unless relationship.loaded?(self) && relationship.get!(self)
-
-
1
parent_relationships << relationship
-
end
-
-
52
parent_relationships
-
end
-
-
# Returns loaded child relationships
-
#
-
# @return [Array<Associations::OneToMany::Relationship>]
-
# array of child relationships for which this resource is parent and is loaded
-
#
-
# @api private
-
1
def child_relationships
-
27
child_relationships = []
-
-
27
relationships.each do |relationship|
-
55
next unless relationship.respond_to?(:collection_for)
-
54
set_default_value(relationship)
-
54
next unless relationship.loaded?(self)
-
-
child_relationships << relationship
-
end
-
-
27
many_to_many, other = child_relationships.partition do |relationship|
-
relationship.kind_of?(Associations::ManyToMany::Relationship)
-
end
-
-
27
many_to_many + other
-
end
-
-
# @api private
-
1
def parent_associations
-
parent_relationships.map { |relationship| relationship.get!(self) }
-
end
-
-
# @api private
-
1
def child_associations
-
27
child_relationships.map { |relationship| relationship.get_collection(self) }
-
end
-
-
# Commit the persisted state
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def _persist
-
20
self.persistence_state = persistence_state.commit
-
end
-
-
# This method executes the hooks before and after resource creation
-
#
-
# @return [Boolean]
-
#
-
# @see Resource#_create
-
#
-
# @api private
-
1
def create_with_hooks
-
20
catch :halt do
-
20
before_save_hook
-
20
before_create_hook
-
20
_persist
-
20
after_create_hook
-
20
after_save_hook
-
end
-
end
-
-
# This method executes the hooks before and after resource updating
-
#
-
# @return [Boolean]
-
#
-
# @see Resource#_update
-
#
-
# @api private
-
1
def update_with_hooks
-
catch :halt do
-
before_save_hook
-
before_update_hook
-
_persist
-
after_update_hook
-
after_save_hook
-
end
-
end
-
-
# Destroy the resource
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def _destroy(execute_hooks = true)
-
self.persistence_state = persistence_state.delete
-
_persist
-
end
-
-
# @api private
-
1
def _save(execute_hooks = true)
-
51
run_once(true) do
-
51
save_parents(execute_hooks) && save_self(execute_hooks) && save_children(execute_hooks)
-
end
-
end
-
-
# Saves the resource
-
#
-
# @return [Boolean]
-
# true if the resource was successfully saved
-
#
-
# @api semipublic
-
1
def save_self(execute_hooks = true)
-
# short-circuit if the resource is not dirty
-
28
return saved? unless dirty_self?
-
-
20
if execute_hooks
-
20
new? ? create_with_hooks : update_with_hooks
-
else
-
_persist
-
end
-
20
clean?
-
end
-
-
# Saves the parent resources
-
#
-
# @return [Boolean]
-
# true if the parents were successfully saved
-
#
-
# @api private
-
1
def save_parents(execute_hooks)
-
52
run_once(true) do
-
parent_relationships.map do |relationship|
-
1
parent = relationship.get(self)
-
-
1
if parent.__send__(:save_parents, execute_hooks) && parent.__send__(:save_self, execute_hooks)
-
1
relationship.set(self, parent) # set the FK values
-
end
-
52
end.all?
-
end
-
end
-
-
# Saves the children resources
-
#
-
# @return [Boolean]
-
# true if the children were successfully saved
-
#
-
# @api private
-
1
def save_children(execute_hooks)
-
child_associations.map do |association|
-
association.__send__(execute_hooks ? :save : :save!)
-
27
end.all?
-
end
-
-
# Checks if the resource has unsaved changes
-
#
-
# @return [Boolean]
-
# true if the resource has unsaved changes
-
#
-
# @api semipublic
-
1
def dirty_self?
-
48
if original_attributes.any?
-
40
true
-
8
elsif new?
-
!model.serial.nil? || properties.any? { |property| property.default? }
-
else
-
8
false
-
end
-
end
-
-
# Checks if the parents have unsaved changes
-
#
-
# @return [Boolean]
-
# true if the parents have unsaved changes
-
#
-
# @api private
-
1
def dirty_parents?
-
run_once(false) do
-
parent_associations.any? do |association|
-
association.__send__(:dirty_self?) || association.__send__(:dirty_parents?)
-
end
-
end
-
end
-
-
# Checks if the children have unsaved changes
-
#
-
# @param [Hash] resources
-
# resources that have already been tested
-
#
-
# @return [Boolean]
-
# true if the children have unsaved changes
-
#
-
# @api private
-
1
def dirty_children?
-
child_associations.any? { |association| association.dirty? }
-
end
-
-
# Return true if +other+'s is equivalent or equal to +self+'s
-
#
-
# @param [Resource] other
-
# The Resource whose attributes are to be compared with +self+'s
-
# @param [Symbol] operator
-
# The comparison operator to use to compare the attributes
-
#
-
# @return [Boolean]
-
# The result of the comparison of +other+'s attributes with +self+'s
-
#
-
# @api private
-
1
def cmp?(other, operator)
-
return false unless repository.send(operator, other.repository) &&
-
2
key.send(operator, other.key)
-
-
if saved? && other.saved?
-
# if dirty attributes match then they are the same resource
-
dirty_attributes == other.dirty_attributes
-
else
-
# compare properties for unsaved resources
-
properties.all? do |property|
-
__send__(property.name).send(operator, other.__send__(property.name))
-
end
-
end
-
end
-
-
# @api private
-
1
def set_default_value(subject)
-
63
return unless persistence_state.respond_to?(:set_default_value, true)
-
9
persistence_state.__send__(:set_default_value, subject)
-
end
-
-
# Execute all the queued up hooks for a given type and name
-
#
-
# @param [Symbol] type
-
# the type of hook to execute (before or after)
-
# @param [Symbol] name
-
# the name of the hook to execute
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def execute_hooks_for(type, name)
-
100
model.hooks[name][type].each { |hook| hook.call(self) }
-
end
-
-
# Raises an exception if #update is performed on a dirty resource
-
#
-
# @param [Symbol] method
-
# the name of the method to use in the exception
-
#
-
# @return [undefined]
-
#
-
# @raise [UpdateConflictError]
-
# raise if the resource is dirty
-
#
-
# @api private
-
1
def assert_update_clean_only(method)
-
if dirty?
-
raise UpdateConflictError, "#{model}##{method} cannot be called on a #{new? ? 'new' : 'dirty'} resource"
-
end
-
end
-
-
# Raises an exception if #save is performed on a destroyed resource
-
#
-
# @param [Symbol] method
-
# the name of the method to use in the exception
-
#
-
# @return [undefined]
-
#
-
# @raise [PersistenceError]
-
# raise if the resource is destroyed
-
#
-
# @api private
-
1
def assert_not_destroyed(method)
-
51
if destroyed?
-
raise PersistenceError, "#{model}##{method} cannot be called on a destroyed resource"
-
end
-
end
-
-
# Raises an exception if #save returns false
-
#
-
# @param [Symbol] method
-
# the name of the method to use in the exception
-
# @param [Boolean] save_result
-
# the result of the #save call
-
#
-
# @return [undefined]
-
#
-
# @raise [SaveFailureError]
-
# raise if the resource was not saved
-
#
-
# @api private
-
1
def assert_save_successful(method, save_retval)
-
51
if save_retval != true && raise_on_save_failure
-
raise SaveFailureError.new("#{model}##{method} returned #{save_retval.inspect}, #{model} was not saved", self)
-
end
-
end
-
-
# Prevent a method from being in the stack more than once
-
#
-
# The purpose of this method is to prevent SystemStackError from
-
# being thrown from methods from encountering infinite recursion
-
# when called on resources having circular dependencies.
-
#
-
# @param [Object] default
-
# default return value
-
#
-
# @yield The block of code to run once
-
#
-
# @return [Object]
-
# block return value
-
#
-
# @api private
-
1
def run_once(default)
-
123
caller_method = Kernel.caller(1).first[/`([^'?!]+)[?!]?'/, 1]
-
123
sentinel = "@_#{caller_method}_sentinel"
-
123
return instance_variable_get(sentinel) if instance_variable_defined?(sentinel)
-
-
123
begin
-
123
instance_variable_set(sentinel, default)
-
123
yield
-
ensure
-
123
remove_instance_variable(sentinel)
-
end
-
end
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
-
# the state of the resource (abstract)
-
1
class PersistenceState
-
1
extend Equalizer
-
-
1
equalize :resource
-
-
1
attr_reader :resource
-
-
1
def initialize(resource)
-
86
@resource = resource
-
86
@model = resource.model
-
end
-
-
1
def get(subject, *args)
-
222
subject.get(resource, *args)
-
end
-
-
1
def set(subject, value)
-
105
subject.set(resource, value)
-
105
self
-
end
-
-
1
def delete
-
raise NotImplementedError, "#{self.class}#delete should be implemented"
-
end
-
-
1
def commit
-
raise NotImplementedError, "#{self.class}#commit should be implemented"
-
end
-
-
1
def rollback
-
raise NotImplementedError, "#{self.class}#rollback should be implemented"
-
end
-
-
1
private
-
-
1
attr_reader :model
-
-
1
def properties
-
40
@properties ||= model.properties(repository.name)
-
end
-
-
1
def relationships
-
40
@relationships ||= model.relationships(repository.name)
-
end
-
-
1
def identity_map
-
20
@identity_map ||= repository.identity_map(model)
-
end
-
-
1
def remove_from_identity_map
-
identity_map.delete(resource.key)
-
end
-
-
1
def add_to_identity_map
-
20
identity_map[resource.key] = resource
-
end
-
-
1
def set_child_keys
-
20
relationships.each do |relationship|
-
41
set_child_key(relationship)
-
end
-
end
-
-
1
def set_child_key(relationship)
-
41
return unless relationship.loaded?(resource) && relationship.respond_to?(:resource_for)
-
1
set(relationship, get(relationship))
-
end
-
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a persisted/unmodified resource
-
1
class Clean < Persisted
-
1
def set(subject, value)
-
if not_modified?(subject, value)
-
self
-
else
-
# assign to persistence_state so that if Dirty#set calls
-
# a Relationship#set, which modifies a Property, the same
-
# Dirty state instance will be reused.
-
state = resource.persistence_state = Dirty.new(resource)
-
state.set(subject, value)
-
end
-
end
-
-
1
def delete
-
Deleted.new(resource)
-
end
-
-
1
def commit
-
self
-
end
-
-
1
def rollback
-
self
-
end
-
-
1
private
-
-
1
def not_modified?(subject, value)
-
subject.loaded?(resource) && subject.get!(resource).eql?(value)
-
end
-
-
end # class Clean
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a persisted/deleted resource
-
1
class Deleted < Persisted
-
1
def set(subject, value)
-
raise ImmutableDeletedError, 'Deleted resource cannot be modified'
-
end
-
-
1
def delete
-
self
-
end
-
-
1
def commit
-
delete_resource
-
remove_from_identity_map
-
Immutable.new(resource)
-
end
-
-
1
private
-
-
1
def delete_resource
-
repository.delete(collection_for_self)
-
end
-
-
end # class Deleted
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a persisted/dirty resource
-
1
class Dirty < Persisted
-
1
def set(subject, value)
-
track(subject, value)
-
super
-
original_attributes.empty? ? Clean.new(resource) : self
-
end
-
-
1
def delete
-
reset_resource
-
Deleted.new(resource)
-
end
-
-
1
def commit
-
remove_from_identity_map
-
set_child_keys
-
return self unless valid_attributes?
-
update_resource
-
reset_original_attributes
-
reset_resource_key
-
Clean.new(resource)
-
ensure
-
add_to_identity_map
-
end
-
-
1
def rollback
-
reset_resource
-
Clean.new(resource)
-
end
-
-
1
def original_attributes
-
@original_attributes ||= {}
-
end
-
-
1
private
-
-
1
def track(subject, value)
-
if original_attributes.key?(subject)
-
# stop tracking if the new value is the same as the original
-
if original_attributes[subject].eql?(value)
-
original_attributes.delete(subject)
-
end
-
elsif !value.eql?(original = get(subject))
-
# track the original value
-
original_attributes[subject] = original
-
end
-
end
-
-
1
def update_resource
-
repository.update(resource.dirty_attributes, collection_for_self)
-
end
-
-
1
def reset_resource
-
reset_resource_properties
-
reset_resource_relationships
-
end
-
-
1
def reset_resource_key
-
resource.instance_eval { remove_instance_variable(:@_key) }
-
end
-
-
1
def reset_resource_properties
-
# delete every original attribute after resetting the resource
-
original_attributes.delete_if do |property, value|
-
property.set!(resource, value)
-
true
-
end
-
end
-
-
1
def reset_resource_relationships
-
relationships.each do |relationship|
-
next unless relationship.loaded?(resource)
-
# TODO: consider a method in Relationship that can reset the relationship
-
resource.instance_eval { remove_instance_variable(relationship.instance_variable_name) }
-
end
-
end
-
-
1
def reset_original_attributes
-
original_attributes.clear
-
end
-
-
1
def valid_attributes?
-
original_attributes.each_key do |property|
-
return false if property.kind_of?(Property) && !property.valid?(property.get!(resource))
-
end
-
true
-
end
-
-
end # class Dirty
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a not-persisted/unmodifiable resource
-
1
class Immutable < PersistenceState
-
1
def get(subject, *args)
-
unless subject.loaded?(resource) || subject.kind_of?(Associations::Relationship)
-
raise ImmutableError, 'Immutable resource cannot be lazy loaded'
-
end
-
-
super
-
end
-
-
1
def set(subject, value)
-
raise ImmutableError, 'Immutable resource cannot be modified'
-
end
-
-
1
def delete
-
raise ImmutableError, 'Immutable resource cannot be deleted'
-
end
-
-
1
def commit
-
self
-
end
-
-
1
def rollback
-
self
-
end
-
-
end # class Immutable
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a persisted resource (abstract)
-
1
class Persisted < PersistenceState
-
1
def get(subject, *args)
-
lazy_load(subject)
-
super
-
end
-
-
1
private
-
-
1
def repository
-
@repository ||= resource.instance_variable_get(:@_repository)
-
end
-
-
1
def collection_for_self
-
@collection_for_self ||= resource.collection_for_self
-
end
-
-
1
def lazy_load(subject)
-
subject.lazy_load(resource)
-
end
-
-
end # class Persisted
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Resource
-
1
class PersistenceState
-
-
# a not-persisted/modifiable resource
-
1
class Transient < PersistenceState
-
1
def get(subject, *args)
-
222
set_default_value(subject)
-
222
super
-
end
-
-
1
def set(subject, value)
-
105
track(subject)
-
105
super
-
end
-
-
1
def delete
-
self
-
end
-
-
1
def commit
-
20
set_child_keys
-
20
set_default_values
-
20
return self unless valid_attributes?
-
20
create_resource
-
20
set_repository
-
20
add_to_identity_map
-
20
Clean.new(resource)
-
end
-
-
1
def rollback
-
self
-
end
-
-
1
def original_attributes
-
201
@original_attributes ||= {}
-
end
-
-
1
private
-
-
1
def repository
-
100
@repository ||= model.repository
-
end
-
-
1
def set_default_values
-
20
(properties | relationships).each do |subject|
-
106
set_default_value(subject)
-
end
-
end
-
-
1
def set_default_value(subject)
-
337
return if subject.loaded?(resource) || !subject.default?
-
default = typecast_default(subject, subject.default_for(resource))
-
set(subject, default)
-
end
-
-
1
def typecast_default(subject, default)
-
return default unless subject.respond_to?(:typecast)
-
-
typecasted_default = subject.send(:typecast, default)
-
unless typecasted_default.eql?(default)
-
warn "Automatic typecasting of default property values is deprecated " +
-
"(#{default.inspect} was casted to #{typecasted_default.inspect}). " +
-
"Specify the correct type for #{resource.class}."
-
end
-
typecasted_default
-
end
-
-
1
def track(subject)
-
105
original_attributes[subject] = nil
-
end
-
-
1
def create_resource
-
20
repository.create([ resource ])
-
end
-
-
1
def set_repository
-
20
resource.instance_variable_set(:@_repository, repository)
-
end
-
-
1
def valid_attributes?
-
20
properties.all? do |property|
-
65
value = get(property)
-
65
property.serial? && value.nil? || property.valid?(value)
-
end
-
end
-
-
end # class Transient
-
end # class PersistenceState
-
end # module Resource
-
end # module DataMapper
-
1
module DataMapper
-
1
module Assertions
-
1
def assert_kind_of(name, value, *klasses)
-
1436
klasses.each { |k| return if value.kind_of?(k) }
-
raise ArgumentError, "+#{name}+ should be #{klasses.map { |k| k.name } * ' or '}, but was #{value.class.name}", caller(2)
-
end
-
end
-
end
-
1
module DataMapper
-
1
module Chainable
-
-
# @api private
-
1
def chainable(&block)
-
4
mod = Module.new(&block)
-
4
include mod
-
4
mod
-
end
-
-
# @api private
-
1
def extendable(&block)
-
4
mod = Module.new(&block)
-
4
extend mod
-
4
mod
-
end
-
end # module Chainable
-
end # module DataMapper
-
1
module DataMapper
-
1
module Deprecate
-
1
def deprecate(old_method, new_method)
-
14
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{old_method}(*args, &block)
-
warn "\#{self.class}##{old_method} is deprecated, use \#{self.class}##{new_method} instead (\#{caller.first})"
-
send(#{new_method.inspect}, *args, &block)
-
end
-
RUBY
-
end
-
end # module Deprecate
-
end # module DataMapper
-
1
module DataMapper
-
1
class DescendantSet
-
1
include Enumerable
-
-
# Initialize a DescendantSet instance
-
#
-
# @param [#to_ary] descendants
-
# initialize with the descendants
-
#
-
# @api private
-
1
def initialize(descendants = [])
-
37
@descendants = SubjectSet.new(descendants)
-
end
-
-
# Copy a DescendantSet instance
-
#
-
# @param [DescendantSet] original
-
# the original descendants
-
#
-
# @api private
-
1
def initialize_copy(original)
-
@descendants = @descendants.dup
-
end
-
-
# Add a descendant
-
#
-
# @param [Module] descendant
-
#
-
# @return [DescendantSet]
-
# self
-
#
-
# @api private
-
1
def <<(descendant)
-
35
@descendants << descendant
-
35
self
-
end
-
-
# Remove a descendant
-
#
-
# Also removes from all descendants
-
#
-
# @param [Module] descendant
-
#
-
# @return [DescendantSet]
-
# self
-
#
-
# @api private
-
1
def delete(descendant)
-
@descendants.delete(descendant)
-
each { |d| d.descendants.delete(descendant) }
-
end
-
-
# Iterate over each descendant
-
#
-
# @yield [descendant]
-
# @yieldparam [Module] descendant
-
#
-
# @return [DescendantSet]
-
# self
-
#
-
# @api private
-
1
def each
-
242
@descendants.each do |descendant|
-
47
yield descendant
-
66
descendant.descendants.each { |dd| yield dd }
-
end
-
239
self
-
end
-
-
# Test if there are any descendants
-
#
-
# @return [Boolean]
-
#
-
# @api private
-
1
def empty?
-
@descendants.empty?
-
end
-
-
# Removes all entries and returns self
-
#
-
# @return [DescendantSet] self
-
#
-
# @api private
-
1
def clear
-
@descendants.clear
-
end
-
-
end # class DescendantSet
-
end # module DataMapper
-
1
module DataMapper
-
1
module Equalizer
-
1
def equalize(*methods)
-
10
define_eql_method(methods)
-
10
define_equivalent_method(methods)
-
10
define_hash_method(methods)
-
end
-
-
1
private
-
-
1
def define_eql_method(methods)
-
10
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def eql?(other)
-
return true if equal?(other)
-
instance_of?(other.class) &&
-
28
#{methods.map { |method| "#{method}.eql?(other.#{method})" }.join(' && ')}
-
end
-
RUBY
-
end
-
-
1
def define_equivalent_method(methods)
-
10
respond_to = []
-
10
equivalent = []
-
-
10
methods.each do |method|
-
28
respond_to << "other.respond_to?(#{method.inspect})"
-
28
equivalent << "#{method} == other.#{method}"
-
end
-
-
10
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def ==(other)
-
return true if equal?(other)
-
return false unless kind_of?(other.class) || other.kind_of?(self.class)
-
#{respond_to.join(' && ')} &&
-
#{equivalent.join(' && ')}
-
end
-
RUBY
-
end
-
-
1
def define_hash_method(methods)
-
10
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def hash
-
28
self.class.hash ^ #{methods.map { |method| "#{method}.hash" }.join(' ^ ')}
-
end
-
RUBY
-
end
-
end
-
end
-
2
module DataMapper; module Ext
-
1
module Array
-
# Transforms an Array of key/value pairs into a {Mash}.
-
#
-
# This is a better idiom than using Mash[*array.flatten] in Ruby 1.8.6
-
# because it is not possible to limit the flattening to a single
-
# level.
-
#
-
# @param [Array] array
-
# The array of key/value pairs to transform.
-
#
-
# @return [Mash]
-
# A {Mash} where each entry in the Array is turned into a key/value.
-
#
-
# @api semipublic
-
1
def self.to_mash(array)
-
m = Mash.new
-
array.each { |k,v| m[k] = v }
-
m
-
end
-
end # class Array
-
end; end
-
1
module DataMapper
-
1
module Ext
-
# Determines whether the specified +value+ is blank.
-
#
-
# An object is blank if it's false, empty, or a whitespace string.
-
# For example, "", " ", +nil+, [], and {} are blank.
-
#
-
# @api semipublic
-
1
def self.blank?(value)
-
560
return value.blank? if value.respond_to?(:blank?)
-
560
case value
-
when ::NilClass, ::FalseClass
-
16
true
-
when ::TrueClass, ::Numeric
-
28
false
-
when ::Array, ::Hash
-
value.empty?
-
when ::String
-
504
value !~ /\S/
-
else
-
12
value.nil? || (value.respond_to?(:empty?) && value.empty?)
-
end
-
end
-
end
-
end
-
2
module DataMapper; module Ext
-
1
module Hash
-
# Creates a hash with *only* the specified key/value pairs from +hash+.
-
#
-
# @param [Hash] hash The hash from which to pick the key/value pairs.
-
# @param [Array] *keys The hash keys to include.
-
#
-
# @return [Hash] A new hash with only the selected keys.
-
#
-
# @example
-
# hash = { :one => 1, :two => 2, :three => 3 }
-
# Ext::Hash.only(hash, :one, :two) # => { :one => 1, :two => 2 }
-
#
-
# @api semipublic
-
1
def self.only(hash, *keys)
-
238
h = {}
-
2113
keys.each {|k| h[k] = hash[k] if hash.has_key?(k) }
-
238
h
-
end
-
-
# Returns a hash that includes everything but the given +keys+.
-
#
-
# @param [Hash] hash The hash from which to pick the key/value pairs.
-
# @param [Array] *keys The hash keys to exclude.
-
#
-
# @return [Hash] A new hash without the specified keys.
-
#
-
# @example
-
# hash = { :one => 1, :two => 2, :three => 3 }
-
# Ext::Hash.except(hash, :one, :two) # => { :three => 3 }
-
#
-
# @api semipublic
-
1
def self.except(hash, *keys)
-
503
self.except!(hash.dup, *keys)
-
end
-
-
# Removes the specified +keys+ from the given +hash+.
-
#
-
# @param [Hash] hash The hash to modify.
-
# @param [Array] *keys The hash keys to exclude.
-
#
-
# @return [Hash] +hash+
-
#
-
# @example
-
# hash = { :one => 1, :two => 2, :three => 3 }
-
# Ext::Hash.except!(hash, :one, :two)
-
# hash # => { :three => 3 }
-
#
-
# @api semipublic
-
1
def self.except!(hash, *keys)
-
4216
keys.each { |key| hash.delete(key) }
-
503
hash
-
end
-
-
# Converts the specified +hash+ to a {Mash}.
-
#
-
# @param [Hash] hash The hash to convert.
-
# @return [Mash] The {Mash} for the specified +hash+.
-
#
-
# @api semipublic
-
1
def self.to_mash(hash)
-
1
h = Mash.new(hash)
-
1
h.default = hash.default
-
1
h
-
end
-
end
-
end; end
-
2
module DataMapper; module Ext
-
1
module Module
-
-
# @api semipublic
-
1
def self.find_const(mod, const_name)
-
8
if const_name[0..1] == '::'
-
DataMapper::Ext::Object.full_const_get(const_name[2..-1])
-
else
-
8
nested_const_lookup(mod, const_name)
-
end
-
end
-
-
1
private
-
-
# Doesn't do any caching since constants can change with remove_const
-
1
def self.nested_const_lookup(mod, const_name)
-
8
unless mod.equal?(::Object)
-
8
constants = []
-
-
8
mod.name.split('::').each do |part|
-
8
const = constants.last || ::Object
-
8
constants << const.const_get(part)
-
end
-
-
8
parts = const_name.split('::')
-
-
# from most to least specific constant, use each as a base and try
-
# to find a constant with the name const_name within them
-
8
constants.reverse_each do |const|
-
# return the nested constant if available
-
8
return const if parts.all? do |part|
-
8
const = if RUBY_VERSION >= '1.9.0'
-
8
const.const_defined?(part, false) ? const.const_get(part, false) : nil
-
else
-
const.const_defined?(part) ? const.const_get(part) : nil
-
end
-
end
-
end
-
end
-
-
# no relative constant found, fallback to an absolute lookup and
-
# use const_missing if not found
-
8
DataMapper::Ext::Object.full_const_get(const_name)
-
end
-
-
end
-
end; end
-
2
module DataMapper; module Ext
-
1
module Object
-
# Returns the value of the specified constant.
-
#
-
# @overload full_const_get(obj, name)
-
# Returns the value of the specified constant in +obj+.
-
# @param [Object] obj The root object used as origin.
-
# @param [String] name The name of the constant to get, e.g. "Merb::Router".
-
#
-
# @overload full_const_get(name)
-
# Returns the value of the fully-qualified constant.
-
# @param [String] name The name of the constant to get, e.g. "Merb::Router".
-
#
-
# @return [Object] The constant corresponding to +name+.
-
#
-
# @api semipublic
-
1
def self.full_const_get(obj, name = nil)
-
8
obj, name = ::Object, obj if name.nil?
-
-
8
list = name.split("::")
-
8
list.shift if DataMapper::Ext.blank?(list.first)
-
8
list.each do |x|
-
# This is required because const_get tries to look for constants in the
-
# ancestor chain, but we only want constants that are HERE
-
8
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
-
end
-
8
obj
-
end
-
-
# Sets the specified constant to the given +value+.
-
#
-
# @overload full_const_set(obj, name)
-
# Sets the specified constant in +obj+ to the given +value+.
-
# @param [Object] obj The root object used as origin.
-
# @param [String] name The name of the constant to set, e.g. "Merb::Router".
-
# @param [Object] value The value to assign to the constant.
-
#
-
# @overload full_const_set(name)
-
# Sets the fully-qualified constant to the given +value+.
-
# @param [String] name The name of the constant to set, e.g. "Merb::Router".
-
# @param [Object] value The value to assign to the constant.
-
#
-
# @return [Object] The constant corresponding to +name+.
-
#
-
# @api semipublic
-
1
def self.full_const_set(obj, name, value = nil)
-
obj, name, value = ::Object, obj, name if value.nil?
-
-
list = name.split("::")
-
toplevel = DataMapper::Ext.blank?(list.first)
-
list.shift if toplevel
-
last = list.pop
-
obj = list.empty? ? ::Object : DataMapper::Ext::Object.full_const_get(list.join("::"))
-
obj.const_set(last, value) if obj && !obj.const_defined?(last)
-
end
-
end
-
end; end
-
2
module DataMapper; module Ext
-
1
module String
-
# Replace sequences of whitespace (including newlines) with either
-
# a single space or remove them entirely (according to param _spaced_).
-
#
-
# compress_lines(<<QUERY)
-
# SELECT name
-
# FROM users
-
# QUERY => "SELECT name FROM users"
-
#
-
# @param [String] string
-
# The input string.
-
#
-
# @param [TrueClass, FalseClass] spaced (default=true)
-
# Determines whether returned string has whitespace collapsed or removed.
-
#
-
# @return [String] The input string with whitespace (including newlines) replaced.
-
#
-
# @api semipublic
-
1
def self.compress_lines(string, spaced = true)
-
230
string.split($/).map { |line| line.strip }.join(spaced ? ' ' : '')
-
end
-
end
-
end; end
-
1
module DataMapper
-
1
module Ext
-
1
def self.try_dup(value)
-
459
case value
-
when ::TrueClass, ::FalseClass, ::NilClass, ::Module, ::Numeric, ::Symbol
-
229
value
-
else
-
230
value.dup
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
#
-
# TODO: Write more documentation!
-
#
-
# Overview
-
# ========
-
#
-
# The Hook module is a very simple set of AOP helpers. Basically, it
-
# allows the developer to specify a method or block that should run
-
# before or after another method.
-
#
-
# Usage
-
# =====
-
#
-
# Halting The Hook Stack
-
#
-
# Inheritance
-
#
-
# Other Goodies
-
#
-
# Please bring up any issues regarding Hooks with carllerche on IRC
-
#
-
1
module Hook
-
-
1
def self.included(base)
-
5
base.extend(ClassMethods)
-
5
base.const_set("CLASS_HOOKS", {}) unless base.const_defined?("CLASS_HOOKS")
-
5
base.const_set("INSTANCE_HOOKS", {}) unless base.const_defined?("INSTANCE_HOOKS")
-
5
base.class_eval do
-
5
class << self
-
5
def method_added(name)
-
53
process_method_added(name, :instance)
-
end
-
-
5
def singleton_method_added(name)
-
7
process_method_added(name, :class)
-
end
-
end
-
end
-
end
-
-
1
module ClassMethods
-
1
extend DataMapper::LocalObjectSpace
-
1
include DataMapper::Assertions
-
# Inject code that executes before the target class method.
-
#
-
# @param target_method<Symbol> the name of the class method to inject before
-
# @param method_sym<Symbol> the name of the method to run before the
-
# target_method
-
# @param block<Block> the code to run before the target_method
-
#
-
# @note
-
# Either method_sym or block is required.
-
# -
-
# @api public
-
1
def before_class_method(target_method, method_sym = nil, &block)
-
install_hook :before, target_method, method_sym, :class, &block
-
end
-
-
#
-
# Inject code that executes after the target class method.
-
#
-
# @param target_method<Symbol> the name of the class method to inject after
-
# @param method_sym<Symbol> the name of the method to run after the target_method
-
# @param block<Block> the code to run after the target_method
-
#
-
# @note
-
# Either method_sym or block is required.
-
# -
-
# @api public
-
1
def after_class_method(target_method, method_sym = nil, &block)
-
install_hook :after, target_method, method_sym, :class, &block
-
end
-
-
#
-
# Inject code that executes before the target instance method.
-
#
-
# @param target_method<Symbol> the name of the instance method to inject before
-
# @param method_sym<Symbol> the name of the method to run before the
-
# target_method
-
# @param block<Block> the code to run before the target_method
-
#
-
# @note
-
# Either method_sym or block is required.
-
# -
-
# @api public
-
1
def before(target_method, method_sym = nil, &block)
-
install_hook :before, target_method, method_sym, :instance, &block
-
end
-
-
#
-
# Inject code that executes after the target instance method.
-
#
-
# @param target_method<Symbol> the name of the instance method to inject after
-
# @param method_sym<Symbol> the name of the method to run after the
-
# target_method
-
# @param block<Block> the code to run after the target_method
-
#
-
# @note
-
# Either method_sym or block is required.
-
# -
-
# @api public
-
1
def after(target_method, method_sym = nil, &block)
-
install_hook :after, target_method, method_sym, :instance, &block
-
end
-
-
# Register a class method as hookable. Registering a method means that
-
# before hooks will be run immediately before the method is invoked and
-
# after hooks will be called immediately after the method is invoked.
-
#
-
# @param hookable_method<Symbol> The name of the class method that should
-
# be hookable
-
# -
-
# @api public
-
1
def register_class_hooks(*hooks)
-
hooks.each { |hook| register_hook(hook, :class) }
-
end
-
-
# Register aninstance method as hookable. Registering a method means that
-
# before hooks will be run immediately before the method is invoked and
-
# after hooks will be called immediately after the method is invoked.
-
#
-
# @param hookable_method<Symbol> The name of the instance method that should
-
# be hookable
-
# -
-
# @api public
-
1
def register_instance_hooks(*hooks)
-
hooks.each { |hook| register_hook(hook, :instance) }
-
end
-
-
# Not yet implemented
-
1
def reset_hook!(target_method, scope)
-
raise NotImplementedError
-
end
-
-
# --- Alright kids... the rest is internal stuff ---
-
-
# Returns the correct HOOKS Hash depending on whether we are
-
# working with class methods or instance methods
-
1
def hooks_with_scope(scope)
-
60
case scope
-
7
when :class then class_hooks
-
53
when :instance then instance_hooks
-
else raise ArgumentError, 'You need to pass :class or :instance as scope'
-
end
-
end
-
-
1
def class_hooks
-
7
self.const_get("CLASS_HOOKS")
-
end
-
-
1
def instance_hooks
-
53
self.const_get("INSTANCE_HOOKS")
-
end
-
-
# Registers a method as hookable. Registering hooks involves the following
-
# process
-
#
-
# * Create a blank entry in the HOOK Hash for the method.
-
# * Define the methods that execute the before and after hook stack.
-
# These methods will be no-ops at first, but everytime a new hook is
-
# defined, the methods will be redefined to incorporate the new hook.
-
# * Redefine the method that is to be hookable so that the hook stacks
-
# are invoked approprietly.
-
1
def register_hook(target_method, scope)
-
if scope == :instance && !method_defined?(target_method)
-
raise ArgumentError, "#{target_method} instance method does not exist"
-
elsif scope == :class && !respond_to?(target_method)
-
raise ArgumentError, "#{target_method} class method does not exist"
-
end
-
-
hooks = hooks_with_scope(scope)
-
-
if hooks[target_method].nil?
-
hooks[target_method] = {
-
# We need to keep track of which class in the Inheritance chain the
-
# method was declared hookable in. Every time a child declares a new
-
# hook for the method, the hook stack invocations need to be redefined
-
# in the original Class. See #define_hook_stack_execution_methods
-
:before => [], :after => [], :in => self
-
}
-
-
define_hook_stack_execution_methods(target_method, scope)
-
define_advised_method(target_method, scope)
-
end
-
end
-
-
# Is the method registered as a hookable in the given scope.
-
1
def registered_as_hook?(target_method, scope)
-
! hooks_with_scope(scope)[target_method].nil?
-
end
-
-
# Generates names for the various utility methods. We need to do this because
-
# the various utility methods should not end in = so, while we're at it, we
-
# might as well get rid of all punctuation.
-
1
def hook_method_name(target_method, prefix, suffix)
-
target_method = target_method.to_s
-
-
case target_method[-1,1]
-
when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}"
-
when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}"
-
when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}"
-
# I add a _nan_ suffix here so that we don't ever encounter
-
# any naming conflicts.
-
else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}"
-
end
-
end
-
-
# This will need to be refactored
-
1
def process_method_added(method_name, scope)
-
60
hooks_with_scope(scope).each do |target_method, hooks|
-
if hooks[:before].any? { |hook| hook[:name] == method_name }
-
define_hook_stack_execution_methods(target_method, scope)
-
end
-
-
if hooks[:after].any? { |hook| hook[:name] == method_name }
-
define_hook_stack_execution_methods(target_method, scope)
-
end
-
end
-
end
-
-
# Defines two methods. One method executes the before hook stack. The other executes
-
# the after hook stack. This method will be called many times during the Class definition
-
# process. It should be called for each hook that is defined. It will also be called
-
# when a hook is redefined (to make sure that the arity hasn't changed).
-
1
def define_hook_stack_execution_methods(target_method, scope)
-
unless registered_as_hook?(target_method, scope)
-
raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method"
-
end
-
-
hooks = hooks_with_scope(scope)
-
-
before_hooks = hooks[target_method][:before]
-
before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n")
-
-
after_hooks = hooks[target_method][:after]
-
after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n")
-
-
before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
-
after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
-
-
hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
#{scope == :class ? 'class << self' : ''}
-
-
private
-
-
remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
-
def #{before_hook_name}(*args)
-
#{before_hooks}
-
end
-
-
remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{after_hook_name} }
-
def #{after_hook_name}(*args)
-
#{after_hooks}
-
end
-
-
#{scope == :class ? 'end' : ''}
-
RUBY
-
end
-
-
# Returns ruby code that will invoke the hook. It checks the arity of the hook method
-
# and passes arguments accordingly.
-
1
def inline_call(method_info, scope)
-
DataMapper::Hook::ClassMethods.hook_scopes << method_info[:from]
-
name = method_info[:name]
-
if scope == :instance
-
args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : ''
-
%(#{name}(#{args}) if self.class <= DataMapper::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
-
else
-
args = respond_to?(name) && method(name).arity != 0 ? '*args' : ''
-
%(#{name}(#{args}) if self <= DataMapper::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
-
end
-
end
-
-
1
def define_advised_method(target_method, scope)
-
args = args_for(method_with_scope(target_method, scope))
-
-
renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised')
-
-
source = <<-EOD
-
def #{target_method}(#{args})
-
retval = nil
-
catch(:halt) do
-
#{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args})
-
retval = #{renamed_target}(#{args})
-
#{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args})
-
retval
-
end
-
end
-
EOD
-
-
if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method }
-
send(:alias_method, renamed_target, target_method)
-
-
proxy_module = Module.new
-
proxy_module.class_eval(source, __FILE__, __LINE__)
-
self.send(:include, proxy_module)
-
else
-
source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}}
-
source = %{class << self\n#{source}\nend} if scope == :class
-
class_eval(source, __FILE__, __LINE__)
-
end
-
end
-
-
# --- Add a hook ---
-
-
1
def install_hook(type, target_method, method_sym, scope, &block)
-
assert_kind_of 'target_method', target_method, Symbol
-
assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil?
-
assert_kind_of 'scope', scope, Symbol
-
-
if !block_given? and method_sym.nil?
-
raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"."
-
end
-
-
if method_sym.to_s[-1,1] == '='
-
raise ArgumentError, "Methods ending in = cannot be hooks"
-
end
-
-
unless [ :class, :instance ].include?(scope)
-
raise ArgumentError, 'You need to pass :class or :instance as scope'
-
end
-
-
if registered_as_hook?(target_method, scope)
-
hooks = hooks_with_scope(scope)
-
-
#if this hook is previously declared in a sibling or cousin we must move the :in class
-
#to the common ancestor to get both hooks to run.
-
if !(hooks[target_method][:in] <=> self)
-
before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
-
after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
-
-
hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
-
def #{before_hook_name}(*args)
-
super
-
end
-
-
remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
-
def #{after_hook_name}(*args)
-
super
-
end
-
RUBY
-
-
while !(hooks[target_method][:in] <=> self) do
-
hooks[target_method][:in] = hooks[target_method][:in].superclass
-
end
-
-
define_hook_stack_execution_methods(target_method, scope)
-
hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)}
-
end
-
else
-
register_hook(target_method, scope)
-
hooks = hooks_with_scope(scope)
-
end
-
-
#if we were passed a block, create a method out of it.
-
if block
-
method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym
-
if scope == :class
-
singleton_class.instance_eval do
-
define_method(method_sym, &block)
-
end
-
else
-
define_method(method_sym, &block)
-
end
-
end
-
-
# Adds method to the stack an redefines the hook invocation method
-
hooks[target_method][type] << { :name => method_sym, :from => self }
-
define_hook_stack_execution_methods(target_method, scope)
-
end
-
-
# --- Helpers ---
-
-
1
def args_for(method)
-
if method.arity == 0
-
"&block"
-
elsif method.arity > 0
-
"_" << (1 .. method.arity).to_a.join(", _") << ", &block"
-
elsif (method.arity + 1) < 0
-
"_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block"
-
else
-
"*args, &block"
-
end
-
end
-
-
1
def method_with_scope(name, scope)
-
case scope
-
when :class then method(name)
-
when :instance then instance_method(name)
-
else raise ArgumentError, 'You need to pass :class or :instance as scope'
-
end
-
end
-
-
1
def quote_method(name)
-
name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
-
end
-
end
-
-
end
-
end
-
1
module DataMapper
-
1
Inflector.inflections do |inflect|
-
1
inflect.plural(/$/, 's')
-
1
inflect.plural(/s$/i, 's')
-
1
inflect.plural(/(ax|test)is$/i, '\1es')
-
1
inflect.plural(/(octop|vir)us$/i, '\1i')
-
1
inflect.plural(/(octop|vir)i$/i, '\1i')
-
1
inflect.plural(/(alias|status)$/i, '\1es')
-
1
inflect.plural(/(bu)s$/i, '\1ses')
-
1
inflect.plural(/(buffal|tomat)o$/i, '\1oes')
-
1
inflect.plural(/([ti])um$/i, '\1a')
-
1
inflect.plural(/([ti])a$/i, '\1a')
-
1
inflect.plural(/sis$/i, 'ses')
-
1
inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
-
1
inflect.plural(/(hive)$/i, '\1s')
-
1
inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
-
1
inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
-
1
inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
-
1
inflect.plural(/([m|l])ouse$/i, '\1ice')
-
1
inflect.plural(/([m|l])ice$/i, '\1ice')
-
1
inflect.plural(/^(ox)$/i, '\1en')
-
1
inflect.plural(/^(oxen)$/i, '\1')
-
1
inflect.plural(/(quiz)$/i, '\1zes')
-
-
1
inflect.singular(/s$/i, '')
-
1
inflect.singular(/(n)ews$/i, '\1ews')
-
1
inflect.singular(/([ti])a$/i, '\1um')
-
1
inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis')
-
1
inflect.singular(/(^analy)ses$/i, '\1sis')
-
1
inflect.singular(/([^f])ves$/i, '\1fe')
-
1
inflect.singular(/(hive)s$/i, '\1')
-
1
inflect.singular(/(tive)s$/i, '\1')
-
1
inflect.singular(/([lr])ves$/i, '\1f')
-
1
inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
-
1
inflect.singular(/(s)eries$/i, '\1eries')
-
1
inflect.singular(/(m)ovies$/i, '\1ovie')
-
1
inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
-
1
inflect.singular(/([m|l])ice$/i, '\1ouse')
-
1
inflect.singular(/(bus)es$/i, '\1')
-
1
inflect.singular(/(o)es$/i, '\1')
-
1
inflect.singular(/(shoe)s$/i, '\1')
-
1
inflect.singular(/(cris|ax|test)es$/i, '\1is')
-
1
inflect.singular(/(octop|vir)i$/i, '\1us')
-
1
inflect.singular(/(alias|status)es$/i, '\1')
-
1
inflect.singular(/^(ox)en/i, '\1')
-
1
inflect.singular(/(vert|ind)ices$/i, '\1ex')
-
1
inflect.singular(/(matr)ices$/i, '\1ix')
-
1
inflect.singular(/(quiz)zes$/i, '\1')
-
1
inflect.singular(/(database)s$/i, '\1')
-
-
1
inflect.irregular('person', 'people')
-
1
inflect.irregular('man', 'men')
-
1
inflect.irregular('child', 'children')
-
1
inflect.irregular('sex', 'sexes')
-
1
inflect.irregular('move', 'moves')
-
1
inflect.irregular('cow', 'kine')
-
-
1
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans))
-
end
-
end
-
1
module DataMapper
-
1
module Inflector
-
# A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional
-
# inflection rules. Examples:
-
#
-
# DataMapper::Inflector.inflections do |inflect|
-
# inflect.plural /^(ox)$/i, '\1\2en'
-
# inflect.singular /^(ox)en/i, '\1'
-
#
-
# inflect.irregular 'octopus', 'octopi'
-
#
-
# inflect.uncountable "equipment"
-
# end
-
#
-
# New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the
-
# pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may
-
# already have been loaded.
-
1
class Inflections
-
1
def self.instance
-
97
@__instance__ ||= new
-
end
-
-
1
attr_reader :plurals, :singulars, :uncountables, :humans
-
-
1
def initialize
-
1
@plurals, @singulars, @uncountables, @humans = [], [], [], []
-
end
-
-
# Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
-
# The replacement should always be a string that may include references to the matched data from the rule.
-
1
def plural(rule, replacement)
-
35
@uncountables.delete(rule) if rule.is_a?(String)
-
35
@uncountables.delete(replacement)
-
35
@plurals.insert(0, [rule, replacement])
-
end
-
-
# Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
-
# The replacement should always be a string that may include references to the matched data from the rule.
-
1
def singular(rule, replacement)
-
32
@uncountables.delete(rule) if rule.is_a?(String)
-
32
@uncountables.delete(replacement)
-
32
@singulars.insert(0, [rule, replacement])
-
end
-
-
# Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
-
# for strings, not regular expressions. You simply pass the irregular in singular and plural form.
-
#
-
# Examples:
-
# irregular 'octopus', 'octopi'
-
# irregular 'person', 'people'
-
1
def irregular(singular, plural)
-
6
@uncountables.delete(singular)
-
6
@uncountables.delete(plural)
-
6
if singular[0,1].upcase == plural[0,1].upcase
-
5
plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
-
5
plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1])
-
5
singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
-
else
-
1
plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
-
1
plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
-
1
plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
-
1
plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
-
1
singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1])
-
1
singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1])
-
end
-
end
-
-
# Add uncountable words that shouldn't be attempted inflected.
-
#
-
# Examples:
-
# uncountable "money"
-
# uncountable "money", "information"
-
# uncountable %w( money information rice )
-
1
def uncountable(*words)
-
1
(@uncountables << words).flatten!
-
end
-
-
# Specifies a humanized form of a string by a regular expression rule or by a string mapping.
-
# When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
-
# When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
-
#
-
# Examples:
-
# human /_cnt$/i, '\1_count'
-
# human "legacy_col_person_name", "Name"
-
1
def human(rule, replacement)
-
@humans.insert(0, [rule, replacement])
-
end
-
-
# Clears the loaded inflections within a given scope (default is <tt>:all</tt>).
-
# Give the scope as a symbol of the inflection type, the options are: <tt>:plurals</tt>,
-
# <tt>:singulars</tt>, <tt>:uncountables</tt>, <tt>:humans</tt>.
-
#
-
# Examples:
-
# clear :all
-
# clear :plurals
-
1
def clear(scope = :all)
-
case scope
-
when :all
-
@plurals, @singulars, @uncountables = [], [], []
-
else
-
instance_variable_set "@#{scope}", []
-
end
-
end
-
end
-
-
# Yields a singleton instance of Inflector::Inflections so you can specify additional
-
# inflector rules.
-
#
-
# Example:
-
# DataMapper::Inflector.inflections do |inflect|
-
# inflect.uncountable "rails"
-
# end
-
1
def inflections
-
97
if block_given?
-
1
yield Inflections.instance
-
else
-
96
Inflections.instance
-
end
-
end
-
-
# Returns the plural form of the word in the string.
-
#
-
# Examples:
-
# "post".pluralize # => "posts"
-
# "octopus".pluralize # => "octopi"
-
# "sheep".pluralize # => "sheep"
-
# "words".pluralize # => "words"
-
# "CamelOctopus".pluralize # => "CamelOctopi"
-
1
def pluralize(word)
-
7
result = word.to_s.dup
-
-
7
if word.empty? || inflections.uncountables.include?(result.downcase)
-
result
-
else
-
252
inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
-
7
result
-
end
-
end
-
-
# The reverse of +pluralize+, returns the singular form of a word in a string.
-
#
-
# Examples:
-
# "posts".singularize # => "post"
-
# "octopi".singularize # => "octopus"
-
# "sheep".singularize # => "sheep"
-
# "word".singularize # => "word"
-
# "CamelOctopi".singularize # => "CamelOctopus"
-
1
def singularize(word)
-
6
result = word.to_s.dup
-
-
60
if inflections.uncountables.any? { |inflection| result =~ /\b(#{inflection})\Z/i }
-
result
-
else
-
198
inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
-
6
result
-
end
-
end
-
-
# Capitalizes the first word and turns underscores into spaces and strips a
-
# trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
-
#
-
# Examples:
-
# "employee_salary" # => "Employee salary"
-
# "author_id" # => "Author"
-
1
def humanize(lower_case_and_underscored_word)
-
70
result = lower_case_and_underscored_word.to_s.dup
-
-
70
inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
-
70
result.gsub(/_id$/, "").gsub(/_/, " ").capitalize
-
end
-
-
# Capitalizes all the words and replaces some characters in the string to create
-
# a nicer looking title. +titleize+ is meant for creating pretty output. It is not
-
# used in the Rails internals.
-
#
-
# +titleize+ is also aliased as as +titlecase+.
-
#
-
# Examples:
-
# "man from the boondocks".titleize # => "Man From The Boondocks"
-
# "x-men: the last stand".titleize # => "X Men: The Last Stand"
-
1
def titleize(word)
-
humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
-
end
-
-
# Create the name of a table like Rails does for models to table names. This method
-
# uses the +pluralize+ method on the last word in the string.
-
#
-
# Examples
-
# "RawScaledScorer".tableize # => "raw_scaled_scorers"
-
# "egg_and_ham".tableize # => "egg_and_hams"
-
# "fancyCategory".tableize # => "fancy_categories"
-
1
def tableize(class_name)
-
pluralize(underscore(class_name))
-
end
-
-
# Create a class name from a plural table name like Rails does for table names to models.
-
# Note that this returns a string and not a Class. (To convert to an actual class
-
# follow +classify+ with +constantize+.)
-
#
-
# Examples:
-
# "egg_and_hams".classify # => "EggAndHam"
-
# "posts".classify # => "Post"
-
#
-
# Singular names are not handled correctly:
-
# "business".classify # => "Busines"
-
1
def classify(table_name)
-
# strip out any leading schema name
-
camelize(singularize(table_name.to_s.sub(/.*\./, '')))
-
end
-
end
-
end
-
1
module DataMapper
-
# The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
-
# and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
-
# in inflections.rb.
-
#
-
# The Rails core team has stated patches for the inflections library will not be accepted
-
# in order to avoid breaking legacy applications which may be relying on errant inflections.
-
# If you discover an incorrect inflection and require it for your application, you'll need
-
# to correct it yourself (explained below).
-
1
module Inflector
-
1
extend self
-
-
# By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
-
# is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
-
#
-
# +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
-
#
-
# Examples:
-
# "active_record".camelize # => "ActiveRecord"
-
# "active_record".camelize(:lower) # => "activeRecord"
-
# "active_record/errors".camelize # => "ActiveRecord::Errors"
-
# "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
-
#
-
# As a rule of thumb you can think of +camelize+ as the inverse of +underscore+,
-
# though there are cases where that does not hold:
-
#
-
# "SSLError".underscore.camelize # => "SslError"
-
1
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
-
9
if first_letter_in_uppercase
-
18
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
-
else
-
lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
-
end
-
end
-
-
# Makes an underscored, lowercase form from the expression in the string.
-
#
-
# Changes '::' to '/' to convert namespaces to paths.
-
#
-
# Examples:
-
# "ActiveRecord".underscore # => "active_record"
-
# "ActiveRecord::Errors".underscore # => active_record/errors
-
#
-
# As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
-
# though there are cases where that does not hold:
-
#
-
# "SSLError".underscore.camelize # => "SslError"
-
1
def underscore(camel_cased_word)
-
84
word = camel_cased_word.to_s.dup
-
84
word.gsub!(/::/, '/')
-
84
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
-
84
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
-
84
word.tr!("-", "_")
-
84
word.downcase!
-
84
word
-
end
-
-
# Replaces underscores with dashes in the string.
-
#
-
# Example:
-
# "puni_puni" # => "puni-puni"
-
1
def dasherize(underscored_word)
-
underscored_word.gsub(/_/, '-')
-
end
-
-
# Removes the module part from the expression in the string.
-
#
-
# Examples:
-
# "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
-
# "Inflections".demodulize # => "Inflections"
-
1
def demodulize(class_name_in_module)
-
79
class_name_in_module.to_s.gsub(/^.*::/, '')
-
end
-
-
# Creates a foreign key name from a class name.
-
# +separate_class_name_and_id_with_underscore+ sets whether
-
# the method should put '_' between the name and 'id'.
-
#
-
# Examples:
-
# "Message".foreign_key # => "message_id"
-
# "Message".foreign_key(false) # => "messageid"
-
# "Admin::Post".foreign_key # => "post_id"
-
1
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
-
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
-
end
-
-
# Ruby 1.9 introduces an inherit argument for Module#const_get and
-
# #const_defined? and changes their default behavior.
-
1
if Module.method(:const_get).arity == 1
-
# Tries to find a constant with the name specified in the argument string:
-
#
-
# "Module".constantize # => Module
-
# "Test::Unit".constantize # => Test::Unit
-
#
-
# The name is assumed to be the one of a top-level constant, no matter whether
-
# it starts with "::" or not. No lexical context is taken into account:
-
#
-
# C = 'outside'
-
# module M
-
# C = 'inside'
-
# C # => 'inside'
-
# "C".constantize # => 'outside', same as ::C
-
# end
-
#
-
# NameError is raised when the name is not in CamelCase or the constant is
-
# unknown.
-
def constantize(camel_cased_word)
-
names = camel_cased_word.split('::')
-
names.shift if names.empty? || names.first.empty?
-
-
constant = Object
-
names.each do |name|
-
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
-
end
-
constant
-
end
-
else
-
1
def constantize(camel_cased_word) #:nodoc:
-
names = camel_cased_word.split('::')
-
names.shift if names.empty? || names.first.empty?
-
-
constant = Object
-
names.each do |name|
-
constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
-
end
-
constant
-
end
-
end
-
-
# Turns a number into an ordinal string used to denote the position in an
-
# ordered sequence such as 1st, 2nd, 3rd, 4th.
-
#
-
# Examples:
-
# ordinalize(1) # => "1st"
-
# ordinalize(2) # => "2nd"
-
# ordinalize(1002) # => "1002nd"
-
# ordinalize(1003) # => "1003rd"
-
1
def ordinalize(number)
-
if (11..13).include?(number.to_i % 100)
-
"#{number}th"
-
else
-
case number.to_i % 10
-
when 1; "#{number}st"
-
when 2; "#{number}nd"
-
when 3; "#{number}rd"
-
else "#{number}th"
-
end
-
end
-
end
-
end
-
end
-
1
class LazyArray # borrowed partially from StrokeDB
-
1
include Enumerable
-
-
1
attr_reader :head, :tail
-
-
1
def first(*args)
-
if lazy_possible?(@head, *args)
-
@head.first(*args)
-
else
-
lazy_load
-
@array.first(*args)
-
end
-
end
-
-
1
def last(*args)
-
if lazy_possible?(@tail, *args)
-
@tail.last(*args)
-
else
-
lazy_load
-
@array.last(*args)
-
end
-
end
-
-
1
def at(index)
-
if index >= 0 && lazy_possible?(@head, index + 1)
-
@head.at(index)
-
elsif index < 0 && lazy_possible?(@tail, index.abs)
-
@tail.at(index)
-
else
-
lazy_load
-
@array.at(index)
-
end
-
end
-
-
1
def fetch(*args, &block)
-
index = args.first
-
-
if index >= 0 && lazy_possible?(@head, index + 1)
-
@head.fetch(*args, &block)
-
elsif index < 0 && lazy_possible?(@tail, index.abs)
-
@tail.fetch(*args, &block)
-
else
-
lazy_load
-
@array.fetch(*args, &block)
-
end
-
end
-
-
1
def values_at(*args)
-
accumulator = []
-
-
lazy_possible = args.all? do |arg|
-
index, length = extract_slice_arguments(arg)
-
-
if index >= 0 && lazy_possible?(@head, index + length)
-
accumulator.concat(head.values_at(*arg))
-
elsif index < 0 && lazy_possible?(@tail, index.abs)
-
accumulator.concat(tail.values_at(*arg))
-
end
-
end
-
-
if lazy_possible
-
accumulator
-
else
-
lazy_load
-
@array.values_at(*args)
-
end
-
end
-
-
1
def index(entry)
-
(lazy_possible?(@head) && @head.index(entry)) || begin
-
lazy_load
-
@array.index(entry)
-
end
-
end
-
-
1
def include?(entry)
-
(lazy_possible?(@tail) && @tail.include?(entry)) ||
-
(lazy_possible?(@head) && @head.include?(entry)) || begin
-
lazy_load
-
@array.include?(entry)
-
end
-
end
-
-
1
def empty?
-
(@tail.nil? || @tail.empty?) &&
-
(@head.nil? || @head.empty?) && begin
-
lazy_load
-
@array.empty?
-
end
-
end
-
-
1
def any?(&block)
-
2
(lazy_possible?(@tail) && @tail.any?(&block)) ||
-
4
(lazy_possible?(@head) && @head.any?(&block)) || begin
-
2
lazy_load
-
2
@array.any?(&block)
-
end
-
end
-
-
1
def [](*args)
-
index, length = extract_slice_arguments(*args)
-
-
if length == 1 && args.size == 1 && args.first.kind_of?(Integer)
-
return at(index)
-
end
-
-
if index >= 0 && lazy_possible?(@head, index + length)
-
@head[*args]
-
elsif index < 0 && lazy_possible?(@tail, index.abs - 1 + length)
-
@tail[*args]
-
else
-
lazy_load
-
@array[*args]
-
end
-
end
-
-
1
alias_method :slice, :[]
-
-
1
def slice!(*args)
-
index, length = extract_slice_arguments(*args)
-
-
if index >= 0 && lazy_possible?(@head, index + length)
-
@head.slice!(*args)
-
elsif index < 0 && lazy_possible?(@tail, index.abs - 1 + length)
-
@tail.slice!(*args)
-
else
-
lazy_load
-
@array.slice!(*args)
-
end
-
end
-
-
1
def []=(*args)
-
index, length = extract_slice_arguments(*args[0..-2])
-
-
if index >= 0 && lazy_possible?(@head, index + length)
-
@head.[]=(*args)
-
elsif index < 0 && lazy_possible?(@tail, index.abs - 1 + length)
-
@tail.[]=(*args)
-
else
-
lazy_load
-
@array.[]=(*args)
-
end
-
end
-
-
1
alias_method :splice, :[]=
-
-
1
def reverse
-
dup.reverse!
-
end
-
-
1
def reverse!
-
# reverse without kicking if possible
-
if loaded?
-
@array = @array.reverse
-
else
-
@head, @tail = @tail.reverse, @head.reverse
-
-
proc = @load_with_proc
-
-
@load_with_proc = lambda do |v|
-
proc.call(v)
-
v.instance_variable_get(:@array).reverse!
-
end
-
end
-
-
self
-
end
-
-
1
def <<(entry)
-
12
if loaded?
-
12
lazy_load
-
12
@array << entry
-
else
-
@tail << entry
-
end
-
12
self
-
end
-
-
1
def concat(other)
-
if loaded?
-
lazy_load
-
@array.concat(other)
-
else
-
@tail.concat(other)
-
end
-
self
-
end
-
-
1
def push(*entries)
-
if loaded?
-
lazy_load
-
@array.push(*entries)
-
else
-
@tail.push(*entries)
-
end
-
self
-
end
-
-
1
def unshift(*entries)
-
if loaded?
-
lazy_load
-
@array.unshift(*entries)
-
else
-
@head.unshift(*entries)
-
end
-
self
-
end
-
-
1
def insert(index, *entries)
-
if index >= 0 && lazy_possible?(@head, index)
-
@head.insert(index, *entries)
-
elsif index < 0 && lazy_possible?(@tail, index.abs - 1)
-
@tail.insert(index, *entries)
-
else
-
lazy_load
-
@array.insert(index, *entries)
-
end
-
self
-
end
-
-
1
def pop(*args)
-
if lazy_possible?(@tail, *args)
-
@tail.pop(*args)
-
else
-
lazy_load
-
@array.pop(*args)
-
end
-
end
-
-
1
def shift(*args)
-
if lazy_possible?(@head, *args)
-
@head.shift(*args)
-
else
-
lazy_load
-
@array.shift(*args)
-
end
-
end
-
-
1
def delete_at(index)
-
if index >= 0 && lazy_possible?(@head, index + 1)
-
@head.delete_at(index)
-
elsif index < 0 && lazy_possible?(@tail, index.abs)
-
@tail.delete_at(index)
-
else
-
lazy_load
-
@array.delete_at(index)
-
end
-
end
-
-
1
def delete_if(&block)
-
if loaded?
-
lazy_load
-
@array.delete_if(&block)
-
else
-
@reapers << block
-
@head.delete_if(&block)
-
@tail.delete_if(&block)
-
end
-
self
-
end
-
-
1
def replace(other)
-
10
mark_loaded
-
10
@array.replace(other)
-
10
self
-
end
-
-
1
def clear
-
mark_loaded
-
@array.clear
-
self
-
end
-
-
1
def to_a
-
lazy_load
-
@array.to_a
-
end
-
-
1
alias_method :to_ary, :to_a
-
-
1
def load_with(&block)
-
@load_with_proc = block
-
self
-
end
-
-
1
def loaded?
-
17
@loaded == true
-
end
-
-
1
def kind_of?(klass)
-
super || @array.kind_of?(klass)
-
end
-
-
1
alias_method :is_a?, :kind_of?
-
-
1
def respond_to?(method, include_private = false)
-
super || @array.respond_to?(method)
-
end
-
-
1
def freeze
-
if loaded?
-
@array.freeze
-
else
-
@head.freeze
-
@tail.freeze
-
end
-
@frozen = true
-
self
-
end
-
-
1
def frozen?
-
@frozen == true
-
end
-
-
1
def ==(other)
-
1
if equal?(other)
-
return true
-
end
-
-
1
unless other.respond_to?(:to_ary)
-
return false
-
end
-
-
# if necessary, convert to something that can be compared
-
1
other = other.to_ary unless other.respond_to?(:[])
-
-
1
cmp?(other, :==)
-
end
-
-
1
def eql?(other)
-
if equal?(other)
-
return true
-
end
-
-
unless other.class.equal?(self.class)
-
return false
-
end
-
-
cmp?(other, :eql?)
-
end
-
-
1
def lazy_possible?(list, need_length = 1)
-
4
!loaded? && need_length <= list.size
-
end
-
-
1
private
-
-
1
def initialize
-
10
@frozen = false
-
10
@loaded = false
-
10
@load_with_proc = lambda { |v| v }
-
10
@head = []
-
10
@tail = []
-
10
@array = []
-
10
@reapers = []
-
end
-
-
1
def initialize_copy(original)
-
@head = DataMapper::Ext.try_dup(@head)
-
@tail = DataMapper::Ext.try_dup(@tail)
-
@array = DataMapper::Ext.try_dup(@array)
-
end
-
-
1
def lazy_load
-
return if loaded?
-
mark_loaded
-
@load_with_proc[self]
-
@array.unshift(*@head)
-
@array.concat(@tail)
-
@head = @tail = nil
-
@reapers.each { |r| @array.delete_if(&r) } if @reapers
-
@array.freeze if frozen?
-
end
-
-
1
def mark_loaded
-
10
@loaded = true
-
end
-
-
##
-
# Extract arguments for #slice an #slice! and return index and length
-
#
-
# @param [Integer, Array(Integer), Range] *args the index,
-
# index and length, or range indicating first and last position
-
#
-
# @return [Integer] the index
-
# @return [Integer,NilClass] the length, if any
-
#
-
# @api private
-
1
def extract_slice_arguments(*args)
-
first_arg, second_arg = args
-
-
if args.size == 2 && first_arg.kind_of?(Integer) && second_arg.kind_of?(Integer)
-
return first_arg, second_arg
-
elsif args.size == 1
-
if first_arg.kind_of?(Integer)
-
return first_arg, 1
-
elsif first_arg.kind_of?(Range)
-
index = first_arg.first
-
length = first_arg.last - index
-
length += 1 unless first_arg.exclude_end?
-
return index, length
-
end
-
end
-
-
raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}", caller(1)
-
end
-
-
1
def each
-
9
lazy_load
-
9
if block_given?
-
11
@array.each { |entry| yield entry }
-
9
self
-
else
-
@array.each
-
end
-
end
-
-
# delegate any not-explicitly-handled methods to @array, if possible.
-
# this is handy for handling methods mixed-into Array like group_by
-
1
def method_missing(method, *args, &block)
-
if @array.respond_to?(method)
-
lazy_load
-
results = @array.send(method, *args, &block)
-
results.equal?(@array) ? self : results
-
else
-
super
-
end
-
end
-
-
1
def cmp?(other, operator)
-
1
unless loaded?
-
# compare the head against the beginning of other. start at index
-
# 0 and incrementally compare each entry. if other is a LazyArray
-
# this has a lesser likelyhood of triggering a lazy load
-
0.upto(@head.size - 1) do |i|
-
return false unless @head[i].__send__(operator, other[i])
-
end
-
-
# compare the tail against the end of other. start at index
-
# -1 and decrementally compare each entry. if other is a LazyArray
-
# this has a lesser likelyhood of triggering a lazy load
-
-1.downto(@tail.size * -1) do |i|
-
return false unless @tail[i].__send__(operator, other[i])
-
end
-
-
lazy_load
-
end
-
-
1
@array.send(operator, other.to_ary)
-
end
-
end
-
1
module DataMapper
-
1
module LocalObjectSpace
-
1
def self.extended(klass)
-
2
(class << klass; self; end).send :attr_accessor, :hook_scopes
-
1
klass.hook_scopes = []
-
end
-
-
1
def object_by_id(object_id)
-
self.hook_scopes.detect { |object| object.object_id == object_id }
-
end
-
end
-
end
-
# ==== Public DataMapper Logger API
-
#
-
# To replace an existing logger with a new one:
-
# DataMapper::Logger.set_log(log{String, IO},level{Symbol, String})
-
#
-
# Available logging levels are
-
# DataMapper::Logger::{ Fatal, Error, Warn, Info, Debug }
-
#
-
# Logging via:
-
# DataMapper.logger.fatal(message<String>,&block)
-
# DataMapper.logger.error(message<String>,&block)
-
# DataMapper.logger.warn(message<String>,&block)
-
# DataMapper.logger.info(message<String>,&block)
-
# DataMapper.logger.debug(message<String>,&block)
-
#
-
# Logging with autoflush:
-
# DataMapper.logger.fatal!(message<String>,&block)
-
# DataMapper.logger.error!(message<String>,&block)
-
# DataMapper.logger.warn!(message<String>,&block)
-
# DataMapper.logger.info!(message<String>,&block)
-
# DataMapper.logger.debug!(message<String>,&block)
-
#
-
# Flush the buffer to
-
# DataMapper.logger.flush
-
#
-
# Remove the current log object
-
# DataMapper.logger.close
-
#
-
# ==== Private DataMapper Logger API
-
#
-
# To initialize the logger you create a new object, proxies to set_log.
-
# DataMapper::Logger.new(log{String, IO},level{Symbol, String})
-
1
module DataMapper
-
-
1
class << self
-
1
attr_accessor :logger
-
end
-
-
1
class Logger
-
-
1
attr_accessor :level
-
1
attr_accessor :delimiter
-
1
attr_accessor :auto_flush
-
1
attr_reader :buffer
-
1
attr_reader :log
-
1
attr_reader :init_args
-
-
# ==== Notes
-
# Ruby (standard) logger levels:
-
# :fatal:: An unhandleable error that results in a program crash
-
# :error:: A handleable error condition
-
# :warn:: A warning
-
# :info:: generic (useful) information about system operation
-
# :debug:: low-level information for developers
-
1
Levels =
-
{
-
:fatal => 7,
-
:error => 6,
-
:warn => 4,
-
:info => 3,
-
:debug => 0
-
}
-
-
1
private
-
-
# Readies a log for writing.
-
#
-
# ==== Parameters
-
# log<IO, String>:: Either an IO object or a name of a logfile.
-
1
def initialize_log(log)
-
1
close if @log # be sure that we don't leave open files laying around.
-
-
1
if log.respond_to?(:write)
-
1
@log = log
-
elsif File.exist?(log)
-
@log = open(log, (File::WRONLY | File::APPEND))
-
@log.sync = true
-
else
-
FileUtils.mkdir_p(File.dirname(log)) unless File.directory?(File.dirname(log))
-
@log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
-
@log.sync = true
-
@log.write("#{Time.now.httpdate} #{delimiter} info #{delimiter} Logfile created\n")
-
end
-
end
-
-
1
public
-
-
# To initialize the logger you create a new object, proxies to set_log.
-
#
-
# ==== Parameters
-
# *args:: Arguments to create the log from. See set_logs for specifics.
-
1
def initialize(*args)
-
1
@init_args = args
-
1
set_log(*args)
-
1
self.auto_flush = true
-
1
DataMapper.logger = self
-
end
-
-
# Replaces an existing logger with a new one.
-
#
-
# ==== Parameters
-
# log<IO, String>:: Either an IO object or a name of a logfile.
-
# log_level<~to_sym>::
-
# The log level from, e.g. :fatal or :info. Defaults to :error in the
-
# production environment and :debug otherwise.
-
# delimiter<String>::
-
# Delimiter to use between message sections. Defaults to " ~ ".
-
# auto_flush<Boolean>::
-
# Whether the log should automatically flush after new messages are
-
# added. Defaults to false.
-
1
def set_log(log, log_level = nil, delimiter = " ~ ", auto_flush = false)
-
1
if log_level && Levels[log_level.to_sym]
-
1
@level = Levels[log_level.to_sym]
-
else
-
@level = Levels[:debug]
-
end
-
1
@buffer = []
-
1
@delimiter = delimiter
-
1
@auto_flush = auto_flush
-
-
1
initialize_log(log)
-
end
-
-
# Flush the entire buffer to the log object.
-
1
def flush
-
return unless @buffer.size > 0
-
@log.write(@buffer.slice!(0..-1).join)
-
end
-
-
# Close and remove the current log object.
-
1
def close
-
flush
-
@log.close if @log.respond_to?(:close) && !@log.tty?
-
@log = nil
-
end
-
-
# Appends a message to the log. The methods yield to an optional block and
-
# the output of this block will be appended to the message.
-
#
-
# ==== Parameters
-
# string<String>:: The message to be logged. Defaults to nil.
-
#
-
# ==== Returns
-
# String:: The resulting message added to the log file.
-
1
def <<(string = nil)
-
message = ""
-
message << delimiter
-
message << string if string
-
message << "\n" unless message[-1] == ?\n
-
@buffer << message
-
flush if @auto_flush
-
-
message
-
end
-
1
alias_method :push, :<<
-
-
# Generate the logging methods for DataMapper.logger for each log level.
-
1
Levels.each_pair do |name, number|
-
5
class_eval <<-LEVELMETHODS, __FILE__, __LINE__
-
-
# Appends a message to the log if the log level is at least as high as
-
# the log level of the logger.
-
#
-
# ==== Parameters
-
# string<String>:: The message to be logged. Defaults to nil.
-
#
-
# ==== Returns
-
# self:: The logger object for chaining.
-
def #{name}(message = nil)
-
self << message if #{number} >= level
-
self
-
end
-
-
# Appends a message to the log if the log level is at least as high as
-
# the log level of the logger. The bang! version of the method also auto
-
# flushes the log buffer to disk.
-
#
-
# ==== Parameters
-
# string<String>:: The message to be logged. Defaults to nil.
-
#
-
# ==== Returns
-
# self:: The logger object for chaining.
-
def #{name}!(message = nil)
-
self << message if #{number} >= level
-
flush if #{number} >= level
-
self
-
end
-
-
# ==== Returns
-
# Boolean:: True if this level will be logged by this logger.
-
def #{name}?
-
#{number} >= level
-
end
-
LEVELMETHODS
-
end
-
-
end
-
-
end
-
1
module DataMapper
-
# This class has dubious semantics and we only have it so that people can write
-
# params[:key] instead of params['key'].
-
1
class Mash < Hash
-
-
# Initializes a new mash.
-
#
-
# @param [Hash, Object] constructor
-
# The default value for the mash. If +constructor+ is a Hash, a new mash
-
# will be created based on the keys of the hash and no default value will
-
# be set.
-
1
def initialize(constructor = {})
-
1
if constructor.is_a?(Hash)
-
1
super()
-
1
update(constructor)
-
else
-
super(constructor)
-
end
-
end
-
-
# Gets the default value for the mash.
-
#
-
# @param [Object] key
-
# The default value for the mash. If +key+ is a Symbol and it is a key in
-
# the mash, then the default value will be set to the value matching the
-
# key.
-
1
def default(key = nil)
-
14
if key.is_a?(Symbol) && include?(key = key.to_s)
-
7
self[key]
-
else
-
7
super
-
end
-
end
-
-
1
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
-
1
alias_method :regular_update, :update unless method_defined?(:regular_update)
-
-
# Sets the +value+ associated with the specified +key+.
-
#
-
# @param [Object] key The key to set.
-
# @param [Object] value The value to set the key to.
-
1
def []=(key, value)
-
1
regular_writer(convert_key(key), convert_value(value))
-
end
-
-
# Updates the mash with the key/value pairs from the specified hash.
-
#
-
# @param [Hash] other_hash
-
# A hash to update values in the mash with. The keys and the values will be
-
# converted to Mash format.
-
#
-
# @return [Mash] The updated mash.
-
1
def update(other_hash)
-
9
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
-
1
self
-
end
-
-
1
alias_method :merge!, :update
-
-
# Determines whether the mash contains the specified +key+.
-
#
-
# @param [Object] key The key to check for.
-
# @return [Boolean] True if the key exists in the mash.
-
1
def key?(key)
-
8
super(convert_key(key))
-
end
-
-
1
alias_method :include?, :key?
-
1
alias_method :has_key?, :key?
-
1
alias_method :member?, :key?
-
-
# @param [Object] key The key to fetch.
-
# @param [Array] *extras Default value.
-
#
-
# @return [Object] The value at key or the default value.
-
1
def fetch(key, *extras)
-
3
super(convert_key(key), *extras)
-
end
-
-
# @param [Array] *indices
-
# The keys to retrieve values for.
-
#
-
# @return [Array] The values at each of the provided keys.
-
1
def values_at(*indices)
-
indices.collect {|key| self[convert_key(key)]}
-
end
-
-
# @param [Hash] hash The hash to merge with the mash.
-
#
-
# @return [Mash] A new mash with the hash values merged in.
-
1
def merge(hash)
-
self.dup.update(hash)
-
end
-
-
# @param [Object] key The key to delete from the mash.
-
1
def delete(key)
-
1
super(convert_key(key))
-
end
-
-
# Returns a mash that includes everything but the given +keys+.
-
#
-
# @param [Array<String, Symbol>] *keys The mash keys to exclude.
-
#
-
# @return [Mash] A new mash without the selected keys.
-
#
-
# @example
-
# { :one => 1, :two => 2, :three => 3 }.except(:one)
-
# #=> { "two" => 2, "three" => 3 }
-
1
def except(*keys)
-
self.dup.except!(*keys.map {|k| convert_key(k)})
-
end
-
-
# Removes the specified +keys+ from the mash.
-
#
-
# @param [Array] *keys The mash keys to exclude.
-
#
-
# @return [Hash] +hash+
-
#
-
# @example
-
# mash = { :one => 1, :two => 2, :three => 3 }
-
# mash.except!(:one, :two)
-
# mash # => { :three => 3 }
-
1
def except!(*keys)
-
keys.each { |key| delete(key) }
-
self
-
end
-
-
# Used to provide the same interface as Hash.
-
#
-
# @return [Mash] This mash unchanged.
-
1
def stringify_keys!; self end
-
-
# @return [Hash] The mash as a Hash with symbolized keys.
-
1
def symbolize_keys
-
h = Hash.new(default)
-
each { |key, val| h[key.to_sym] = val }
-
h
-
end
-
-
# @return [Hash] The mash as a Hash with string keys.
-
1
def to_hash
-
Hash.new(default).replace(self)
-
end
-
-
1
protected
-
# @param [Object] key The key to convert.
-
#
-
# @param [Object]
-
# The converted key. If the key was a symbol, it will be converted to a
-
# string.
-
#
-
# @api private
-
1
def convert_key(key)
-
21
key.kind_of?(Symbol) ? key.to_s : key
-
end
-
-
# @param [Object] value The value to convert.
-
#
-
# @return [Object]
-
# The converted value. A Hash or an Array of hashes, will be converted to
-
# their Mash equivalents.
-
#
-
# @api private
-
1
def convert_value(value)
-
9
if value.class == Hash
-
mash = Mash.new(value)
-
mash.default = value.default
-
mash
-
9
elsif value.is_a?(Array)
-
value.collect { |e| convert_value(e) }
-
else
-
9
value
-
end
-
end
-
end
-
end
-
1
module DataMapper
-
-
# Use these modules to establish naming conventions.
-
# The default is UnderscoredAndPluralized.
-
# You assign a naming convention like so:
-
#
-
# repository(:default).adapter.resource_naming_convention = NamingConventions::Resource::Underscored
-
#
-
# You can also easily assign a custom convention with a Proc:
-
#
-
# repository(:default).adapter.resource_naming_convention = lambda do |value|
-
# 'tbl' + value.camelize(true)
-
# end
-
#
-
# Or by simply defining your own module in NamingConventions that responds to
-
# ::call.
-
#
-
# NOTE: It's important to set the convention before accessing your models
-
# since the resource_names are cached after first accessed.
-
# DataMapper.setup(name, uri) returns the Adapter for convenience, so you can
-
# use code like this:
-
#
-
# adapter = DataMapper.setup(:default, 'mock://localhost/mock')
-
# adapter.resource_naming_convention = NamingConventions::Resource::Underscored
-
1
module NamingConventions
-
-
1
module Resource
-
-
1
module UnderscoredAndPluralized
-
1
def self.call(name)
-
5
DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(name)).gsub('/', '_')
-
end
-
end # module UnderscoredAndPluralized
-
-
1
module UnderscoredAndPluralizedWithoutModule
-
1
def self.call(name)
-
DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(DataMapper::Inflector.demodulize(name)))
-
end
-
end # module UnderscoredAndPluralizedWithoutModule
-
-
1
module UnderscoredAndPluralizedWithoutLeadingModule
-
1
def self.call(name)
-
UnderscoredAndPluralized.call(name.to_s.gsub(/^[^:]*::/,''))
-
end
-
end
-
-
1
module Underscored
-
1
def self.call(name)
-
DataMapper::Inflector.underscore(name)
-
end
-
end # module Underscored
-
-
1
module Yaml
-
1
def self.call(name)
-
"#{DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(name))}.yaml"
-
end
-
end # module Yaml
-
-
end # module Resource
-
-
1
module Field
-
-
1
module UnderscoredAndPluralized
-
1
def self.call(property)
-
DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(property.name.to_s)).gsub('/', '_')
-
end
-
end # module UnderscoredAndPluralized
-
-
1
module UnderscoredAndPluralizedWithoutModule
-
1
def self.call(property)
-
DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(DataMapper::Inflector.demodulize(property.name.to_s)))
-
end
-
end # module UnderscoredAndPluralizedWithoutModule
-
-
1
module Underscored
-
1
def self.call(property)
-
20
DataMapper::Inflector.underscore(property.name.to_s)
-
end
-
end # module Underscored
-
-
1
module Yaml
-
1
def self.call(property)
-
"#{DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(property.name.to_s))}.yaml"
-
end
-
end # module Yaml
-
-
end # module Field
-
-
end # module NamingConventions
-
end # module DataMapper
-
1
module DataMapper
-
-
# An ordered set of things
-
#
-
# {OrderedSet} implements set behavior and keeps
-
# track of the order in which entries were added.
-
#
-
# {OrderedSet} allows to inject a class that implements
-
# {OrderedSet::Cache::API} at construction time, and will
-
# use that cache implementation to enforce set semantics
-
# and perform internal caching of insertion order.
-
#
-
# @see OrderedSet::Cache::API
-
# @see OrderedSet::Cache
-
# @see SubjectSet::NameCache
-
#
-
# @api private
-
1
class OrderedSet
-
-
# The default cache used by {OrderedSet}
-
#
-
# Uses a {Hash} as internal storage and enforces set semantics
-
# by calling #eql? and #hash on the set's entries.
-
#
-
# @api private
-
1
class Cache
-
-
# The default implementation of the {API} that {OrderedSet} expects from
-
# the cache object that it uses to
-
#
-
# 1. keep track of insertion order
-
# 2. enforce set semantics.
-
#
-
# Classes including {API} must customize the behavior of the cache in 2 ways:
-
#
-
# They must determine the value to use as cache key and thus set discriminator,
-
# by implementing the {#key_for} method. The {#key_for} method accepts an arbitrary
-
# object as param and the method is free to return whatever value from that method.
-
# Obviously this will most likely be some attribute or value otherwise derived from
-
# the object that got passed in.
-
#
-
# They must determine which objects are valid set entries by overwriting the
-
# {#valid?} method. The {#valid?} method accepts an arbitrary object as param and
-
# the overwriting method must return either true or false.
-
#
-
# The motivation behind this is that set semantics cannot always be enforced
-
# by calling {#eql?} and {#hash} on the set's entries. For example, two entries
-
# might be considered unique wrt the set if their names are the same, but other
-
# internal state differs. This is exactly the case for {DataMapper::Property} and
-
# {DataMapper::Associations::Relationship} objects.
-
#
-
# @see DataMapper::SubjectSet::NameCache
-
#
-
# @api private
-
1
module API
-
-
# Initialize a new Cache
-
#
-
# @api private
-
1
def initialize
-
124
@cache = {}
-
end
-
-
# Tests if the given entry qualifies to be added to the cache
-
#
-
# @param [Object] entry
-
# the entry to be checked
-
#
-
# @return [Boolean]
-
# true if the entry qualifies to be added to the cache
-
#
-
# @api private
-
1
def valid?(entry)
-
raise NotImplementedError, "#{self}#valid? must be implemented"
-
end
-
-
# Given an entry, return the key to be used in the cache
-
#
-
# @param [Object] entry
-
# the entry to get the key for
-
#
-
# @return [Object, nil]
-
# a value derived from the entry that is used as key in the cache
-
#
-
# @api private
-
1
def key_for(entry)
-
raise NotImplementedError, "#{self}#key_for must be implemented"
-
end
-
-
# Check if the entry exists in the cache
-
#
-
# @param [Object] entry
-
# the entry to test for
-
#
-
# @return [Boolean]
-
# true if entry is included in the cache
-
#
-
# @api private
-
1
def include?(entry)
-
@cache.has_key?(key_for(entry))
-
end
-
-
# Return the index for the entry in the cache
-
#
-
# @param [Object] entry
-
# the entry to get the index for
-
#
-
# @return [Integer, nil]
-
# the index for the entry, or nil if it does not exist
-
#
-
# @api private
-
1
def [](entry)
-
329
@cache[key_for(entry)]
-
end
-
-
# Set the index for the entry in the cache
-
#
-
# @param [Object] entry
-
# the entry to set the index for
-
# @param [Integer] index
-
# the index to assign to the given entry
-
#
-
# @return [Integer]
-
# the given index for the entry
-
#
-
# @api private
-
1
def []=(entry, index)
-
329
if valid?(entry)
-
329
@cache[key_for(entry)] = index
-
end
-
end
-
-
# Delete an entry from the cache
-
#
-
# @param [Object] entry
-
# the entry to delete from the cache
-
#
-
# @return [API] self
-
#
-
# @api private
-
1
def delete(entry)
-
deleted_index = @cache.delete(key_for(entry))
-
if deleted_index
-
@cache.each do |key, index|
-
@cache[key] -= 1 if index > deleted_index
-
end
-
end
-
deleted_index
-
end
-
-
# Removes all entries and returns self
-
#
-
# @return [API] self
-
#
-
# @api private
-
1
def clear
-
@cache.clear
-
self
-
end
-
-
end # module API
-
-
1
include API
-
-
# Tests if the given entry qualifies to be added to the cache
-
#
-
# @param [Object] entry
-
# the entry to be checked
-
#
-
# @return [true] true
-
#
-
# @api private
-
1
def valid?(entry)
-
92
true
-
end
-
-
# Given an entry, return the key to be used in the cache
-
#
-
# @param [Object] entry
-
# the entry to get the key for
-
#
-
# @return [Object]
-
# the passed in entry
-
#
-
# @api private
-
1
def key_for(entry)
-
184
entry
-
end
-
-
end # class Cache
-
-
1
include Enumerable
-
1
extend Equalizer
-
-
# This set's entries
-
#
-
# The order in this Array is not guaranteed
-
# to be the order in which the entries were
-
# inserted. Use #each to access the entries
-
# in insertion order.
-
#
-
# @return [Array]
-
# this set's entries
-
#
-
# @api private
-
1
attr_reader :entries
-
-
1
equalize :entries
-
-
# Initialize an OrderedSet
-
#
-
# @param [#each] entries
-
# the entries to initialize this set with
-
# @param [Class<Cache::API>] cache
-
# the cache implementation to use
-
#
-
# @api private
-
1
def initialize(entries = [], cache = Cache)
-
124
@cache = cache.new
-
124
@entries = []
-
124
merge(entries.to_ary)
-
end
-
-
# Initialize a copy of OrderedSet
-
#
-
# @api private
-
1
def initialize_copy(*)
-
83
@cache = @cache.dup
-
83
@entries = @entries.dup
-
end
-
-
# Get the entry at the given index
-
#
-
# @param [Integer] index
-
# the index of the desired entry
-
#
-
# @return [Object, nil]
-
# the entry at the given index, or nil if no entry is present
-
#
-
# @api private
-
1
def [](index)
-
entries[index]
-
end
-
-
# Add or update an entry in the set
-
#
-
# If the entry to add isn't part of the set already,
-
# it will be added. If an entry with the same cache
-
# key as the entry to add is part of the set already,
-
# it will be replaced with the given entry.
-
#
-
# @param [Object] entry
-
# the entry to be added
-
#
-
# @return [OrderedSet] self
-
#
-
# @api private
-
1
def <<(entry)
-
329
if index = @cache[entry]
-
entries[index] = entry
-
else
-
329
@cache[entry] = size
-
329
entries << entry
-
end
-
329
self
-
end
-
-
# Merge with another Enumerable object
-
#
-
# @param [#each] other
-
# the Enumerable to merge with this OrderedSet
-
#
-
# @return [OrderedSet] self
-
#
-
# @api private
-
1
def merge(other)
-
294
other.each { |entry| self << entry }
-
124
self
-
end
-
-
# Delete an entry from this OrderedSet
-
#
-
# @param [Object] entry
-
# the entry to delete
-
#
-
# @return [Object, nil]
-
# the deleted entry or nil
-
#
-
# @api private
-
1
def delete(entry)
-
if index = @cache.delete(entry)
-
entries.delete_at(index)
-
end
-
end
-
-
# Removes all entries and returns self
-
#
-
# @return [OrderedSet] self
-
#
-
# @api private
-
1
def clear
-
@cache.clear
-
entries.clear
-
self
-
end
-
-
# Iterate over each entry in the set
-
#
-
# @yield [entry]
-
# all entries in the set
-
#
-
# @yieldparam [Object] entry
-
# an entry in the set
-
#
-
# @return [OrderedSet] self
-
#
-
# @api private
-
1
def each
-
2254
return to_enum unless block_given?
-
8379
entries.each { |entry| yield(entry) }
-
1441
self
-
end
-
-
# The number of entries
-
#
-
# @return [Integer]
-
# the number of entries
-
#
-
# @api private
-
1
def size
-
358
entries.size
-
end
-
-
# Check if there are any entries
-
#
-
# @return [Boolean]
-
# true if the set is empty
-
#
-
# @api private
-
1
def empty?
-
20
entries.empty?
-
end
-
-
# Check if the entry exists in the set
-
#
-
# @param [Object] entry
-
# the entry to test for
-
#
-
# @return [Boolean]
-
# true if entry is included in the set
-
#
-
# @api private
-
1
def include?(entry)
-
224
entries.include?(entry)
-
end
-
-
# Return the index for the entry in the set
-
#
-
# @param [Object] entry
-
# the entry to check the set for
-
#
-
# @return [Integer, nil]
-
# the index for the entry, or nil if it does not exist
-
#
-
# @api private
-
1
def index(entry)
-
@cache[entry]
-
end
-
-
# Convert the OrderedSet into an Array
-
#
-
# @return [Array]
-
# an array containing all the OrderedSet's entries
-
#
-
# @api private
-
1
def to_ary
-
entries
-
end
-
-
end # class OrderedSet
-
end # module DataMapper
-
1
module DataMapper
-
1
module Subject
-
# Returns a default value of the subject for given resource
-
#
-
# When default value is a callable object, it is called with resource
-
# and subject passed as arguments.
-
#
-
# @param [Resource] resource
-
# the model instance for which the default is to be set
-
#
-
# @return [Object]
-
# the default value of this subject for +resource+
-
#
-
# @api semipublic
-
1
def default_for(resource)
-
if @default.respond_to?(:call)
-
@default.call(resource, self)
-
else
-
DataMapper::Ext.try_dup(@default)
-
end
-
end
-
-
# Returns true if the subject has a default value
-
#
-
# @return [Boolean]
-
# true if the subject has a default value
-
#
-
# @api semipublic
-
1
def default?
-
96
@options.key?(:default)
-
end
-
end
-
end
-
1
module DataMapper
-
-
# An insertion ordered set of named objects
-
#
-
# {SubjectSet} uses {DataMapper::OrderedSet}
-
# under the hood to keep track of a set of
-
# entries. In DataMapper code, a subject
-
# can be either a {DataMapper::Property}, or
-
# a {DataMapper::Associations::Relationship}.
-
#
-
# All entries added to instances of this
-
# class must respond to the {#name} method
-
#
-
# The motivation behind this is that we
-
# use this class as a base to keep track
-
# properties and relationships.
-
# The following constraints apply for these
-
# types of objects: {Property} names must be
-
# unique within any model.
-
# {Associations::Relationship} names must be
-
# unique within any model
-
#
-
# When adding an entry with a name that
-
# already exists, the already existing
-
# entry will be replaced with the new
-
# entry with the same name. This is because
-
# we want to be able to update properties,
-
# and relationship during the course of
-
# initializing our application.
-
#
-
# This also happens to be consistent with
-
# the way ruby handles redefining methods,
-
# where the last definitions "wins".
-
#
-
# Furthermore, the builtin ruby {Set#<<} method
-
# also updates the old object if a new object
-
# gets added.
-
#
-
# @api private
-
1
class SubjectSet
-
-
# An {OrderedSet::Cache::API} implementation that establishes
-
# set semantics based on the name of its entries. The cache
-
# uses the entries' names as cache key and refuses to add
-
# entries that don't respond_to?(:name).
-
#
-
# @see OrderedSet::Cache::API
-
#
-
# @api private
-
1
class NameCache
-
-
1
include OrderedSet::Cache::API
-
-
# Tests if the given entry qualifies to be added to the cache
-
#
-
# @param [#name] entry
-
# the entry to be checked
-
#
-
# @return [Boolean]
-
# true if the entry respond_to?(:name)
-
#
-
# @api private
-
1
def valid?(entry)
-
711
entry.respond_to?(:name)
-
end
-
-
# Given an entry, return the key to be used in the cache
-
#
-
# @param [#name] entry
-
# the entry to get the key for
-
#
-
# @return [#to_s, nil]
-
# the entry's name or nil if the entry isn't #valid?
-
#
-
# @api private
-
1
def key_for(entry)
-
474
valid?(entry) ? entry.name : nil
-
end
-
-
end # class NameCache
-
-
1
include Enumerable
-
-
# The elements in the SubjectSet
-
#
-
# @return [OrderedSet]
-
#
-
# @api private
-
1
attr_reader :entries
-
-
# Initialize a SubjectSet
-
#
-
# @param [Enumerable<#name>] entries
-
# the entries to initialize this set with
-
#
-
# @api private
-
1
def initialize(entries = [])
-
98
@entries = OrderedSet.new(entries, NameCache)
-
end
-
-
# Initialize a copy of a SubjectSet
-
#
-
# @api private
-
1
def initialize_copy(*)
-
83
@entries = @entries.dup
-
end
-
-
# Make sure that entry is part of this SubjectSet
-
#
-
# If an entry with the same name already exists, it
-
# will be updated. If no such named entry exists, it
-
# will be added.
-
#
-
# @param [#name] entry
-
# the entry to be added
-
#
-
# @return [SubjectSet] self
-
#
-
# @api private
-
1
def <<(entry)
-
67
entries << entry
-
67
self
-
end
-
-
# Delete an entry from this SubjectSet
-
#
-
# @param [#name] entry
-
# the entry to delete
-
#
-
# @return [#name, nil]
-
# the deleted entry or nil
-
#
-
# @api private
-
1
def delete(entry)
-
entries.delete(entry)
-
end
-
-
# Removes all entries and returns self
-
#
-
# @return [SubjectSet] self
-
#
-
# @api private
-
1
def clear
-
entries.clear
-
self
-
end
-
-
# Test if the given entry is included in this SubjectSet
-
#
-
# @param [#name] entry
-
# the entry to test for
-
#
-
# @return [Boolean]
-
# true if the entry is included in this SubjectSet
-
#
-
# @api private
-
1
def include?(entry)
-
132
entries.include?(entry)
-
end
-
-
# Tests wether the SubjectSet contains a entry named name
-
#
-
# @param [#to_s] name
-
# the entry name to test for
-
#
-
# @return [Boolean]
-
# true if the SubjectSet contains a entry named name
-
#
-
# @api private
-
1
def named?(name)
-
172
!self[name].nil?
-
end
-
-
# Check if there are any entries
-
#
-
# @return [Boolean]
-
# true if the set contains at least one entry
-
#
-
# @api private
-
1
def empty?
-
20
entries.empty?
-
end
-
-
# Lookup an entry in the SubjectSet based on a given name
-
#
-
# @param [#to_s] name
-
# the name of the entry
-
#
-
# @return [Object, nil]
-
# the entry having the given name, or nil if not found
-
#
-
# @api private
-
1
def [](name)
-
913
name = name.to_s
-
3889
entries.detect { |entry| entry.name.to_s == name }
-
end
-
-
# Iterate over each entry in the set
-
#
-
# @yield [entry]
-
# each entry in the set
-
#
-
# @yieldparam [#name] entry
-
# an entry in the set
-
#
-
# @return [SubjectSet] self
-
#
-
# @api private
-
1
def each
-
1289
return to_enum unless block_given?
-
3749
entries.each { |entry| yield(entry) }
-
1263
self
-
end
-
-
# All entries (or nil values) that have any of the given names
-
#
-
# @param [Enumerable<#to_s>] names
-
# the names of the desired entries
-
#
-
# @return [Array<#name, nil>]
-
# an array containing entries whose names match any of the given
-
# names, or nil values for those names with no matching entries
-
# in the set
-
#
-
# @api private
-
1
def values_at(*names)
-
10
names.map { |name| self[name] }
-
end
-
-
# Get the number of elements inside this SubjectSet
-
#
-
# @return [Integer]
-
# the number of elements
-
#
-
# @api private
-
1
def size
-
29
entries.size
-
end
-
-
# Convert the SubjectSet into an Array
-
#
-
# @return [Array]
-
# an array containing all the SubjectSet's entries
-
#
-
# @api private
-
1
def to_ary
-
11
to_a
-
end
-
-
end # class SubjectSet
-
end # module DataMapper
-
1
module DataMapper
-
1
VERSION = '1.2.1'
-
end
-
1
require 'dm-do-adapter/adapter'
-
1
require 'data_objects'
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
module Adapters
-
-
# DataObjectsAdapter is the base class for all adapers for relational
-
# databases. If you want to add support for a new RDBMS, it makes
-
# sense to make your adapter class inherit from this class.
-
#
-
# By inheriting from DataObjectsAdapter, you get a copy of all the
-
# standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
-
# You can extend and overwrite these copies without affecting the originals.
-
1
class DataObjectsAdapter < AbstractAdapter
-
1
extend Chainable
-
1
extend Deprecate
-
-
1
deprecate :query, :select
-
-
# Retrieve results using an SQL SELECT statement
-
#
-
# @param [String] statement
-
# the SQL SELECT statement
-
# @param [Array] *bind_values
-
# optional bind values to merge into the statement
-
#
-
# @return [Array]
-
# if fields > 1, return an Array of Struct objects
-
# if fields == 1, return an Array of objects
-
#
-
# @api public
-
1
def select(statement, *bind_values)
-
71
with_connection do |connection|
-
71
reader = connection.create_command(statement).execute_reader(*bind_values)
-
71
fields = reader.fields
-
-
71
begin
-
71
if fields.size > 1
-
select_fields(reader, fields)
-
else
-
71
select_field(reader)
-
end
-
ensure
-
71
reader.close
-
end
-
end
-
end
-
-
# Execute non-SELECT SQL query
-
#
-
# @param [String] statement
-
# the SQL statement
-
# @param [Array] *bind_values
-
# optional bind values to merge into the statement
-
#
-
# @return [DataObjects::Result]
-
# result with number of affected rows, and insert id if any
-
#
-
# @api public
-
1
def execute(statement, *bind_values)
-
35
with_connection do |connection|
-
35
command = connection.create_command(statement)
-
35
command.execute_non_query(*bind_values)
-
end
-
end
-
-
# For each model instance in resources, issues an SQL INSERT
-
# (or equivalent) statement to create a new record in the data store for
-
# the instance
-
#
-
# Note that this method does not update the identity map. If a plugin
-
# needs to use an adapter directly, it is up to plugin developer to
-
# keep the identity map up to date.
-
#
-
# @param [Enumerable(Resource)] resources
-
# The list of resources (model instances) to create
-
#
-
# @return [Integer]
-
# The number of records that were actually saved into the database
-
#
-
# @api semipublic
-
1
def create(resources)
-
20
name = self.name
-
-
20
resources.each do |resource|
-
20
model = resource.model
-
20
serial = model.serial(name)
-
20
attributes = resource.dirty_attributes
-
-
20
properties = []
-
20
bind_values = []
-
-
# make the order of the properties consistent
-
20
model.properties(name).each do |property|
-
65
next unless attributes.key?(property)
-
-
45
bind_value = attributes[property]
-
-
# skip insering NULL for columns that are serial or without a default
-
45
next if bind_value.nil? && (property.serial? || !property.default?)
-
-
# if serial is being set explicitly, do not set it again
-
45
if property.equal?(serial)
-
serial = nil
-
end
-
-
45
properties << property
-
45
bind_values << bind_value
-
end
-
-
20
statement = insert_statement(model, properties, serial)
-
-
20
result = with_connection do |connection|
-
20
connection.create_command(statement).execute_non_query(*bind_values)
-
end
-
-
20
if result.affected_rows == 1 && serial
-
20
serial.set!(resource, result.insert_id)
-
end
-
end
-
end
-
-
# Constructs and executes SELECT query, then instantiates
-
# one or many object from result set.
-
#
-
# @param [Query] query
-
# composition of the query to perform
-
#
-
# @return [Array]
-
# result set of the query
-
#
-
# @api semipublic
-
1
def read(query)
-
83
fields = query.fields
-
222
types = fields.map { |property| property.primitive }
-
-
83
statement, bind_values = select_statement(query)
-
-
83
records = []
-
-
83
with_connection do |connection|
-
83
command = connection.create_command(statement)
-
83
command.set_types(types)
-
-
# Handle different splat semantics for nil on 1.8 and 1.9
-
83
reader = if bind_values
-
83
command.execute_reader(*bind_values)
-
else
-
command.execute_reader
-
end
-
-
83
begin
-
83
while reader.next!
-
34
records << Hash[ fields.zip(reader.values) ]
-
end
-
ensure
-
83
reader.close
-
end
-
end
-
-
83
records
-
end
-
-
# Constructs and executes UPDATE statement for given
-
# attributes and a query
-
#
-
# @param [Hash(Property => Object)] attributes
-
# hash of attribute values to set, keyed by Property
-
# @param [Collection] collection
-
# collection of records to be updated
-
#
-
# @return [Integer]
-
# the number of records updated
-
#
-
# @api semipublic
-
1
def update(attributes, collection)
-
query = collection.query
-
-
properties = []
-
bind_values = []
-
-
# make the order of the properties consistent
-
query.model.properties(name).each do |property|
-
next unless attributes.key?(property)
-
properties << property
-
bind_values << attributes[property]
-
end
-
-
statement, conditions_bind_values = update_statement(properties, query)
-
-
bind_values.concat(conditions_bind_values)
-
-
with_connection do |connection|
-
connection.create_command(statement).execute_non_query(*bind_values)
-
end.affected_rows
-
end
-
-
# Constructs and executes DELETE statement for given query
-
#
-
# @param [Collection] collection
-
# collection of records to be deleted
-
#
-
# @return [Integer]
-
# the number of records deleted
-
#
-
# @api semipublic
-
1
def delete(collection)
-
query = collection.query
-
statement, bind_values = delete_statement(query)
-
-
with_connection do |connection|
-
connection.create_command(statement).execute_non_query(*bind_values)
-
end.affected_rows
-
end
-
-
1
protected
-
-
# @api private
-
1
def normalized_uri
-
@normalized_uri ||=
-
begin
-
1
keys = [
-
:adapter, :user, :password, :host, :port, :path, :fragment,
-
:scheme, :query, :username, :database ]
-
1
query = DataMapper::Ext::Hash.except(@options, keys)
-
1
query = nil if query.empty?
-
-
# Better error message in case port is no Numeric value
-
1
port = @options[:port].nil? ? nil : @options[:port].to_int
-
-
DataObjects::URI.new(
-
:scheme => @options[:adapter],
-
:user => @options[:user] || @options[:username],
-
:password => @options[:password],
-
:host => @options[:host],
-
:port => port,
-
:path => @options[:path] || @options[:database],
-
:query => query,
-
:fragment => @options[:fragment]
-
1
).freeze
-
227
end
-
end
-
-
1
chainable do
-
1
protected
-
-
# Instantiates new connection object
-
#
-
# @api semipublic
-
1
def open_connection
-
226
DataObjects::Connection.new(normalized_uri)
-
end
-
-
# Takes connection and closes it
-
#
-
# @api semipublic
-
1
def close_connection(connection)
-
226
connection.close if connection.respond_to?(:close)
-
end
-
end
-
-
1
private
-
-
# @api public
-
1
def initialize(name, uri_or_options)
-
1
super
-
-
# Default the driver-specific logger to DataMapper's logger
-
1
if driver_module = DataObjects.const_get(normalized_uri.scheme.capitalize)
-
1
driver_module.logger = DataMapper.logger if driver_module.respond_to?(:logger=)
-
end
-
end
-
-
# @api private
-
1
def with_connection
-
226
yield connection = open_connection
-
rescue Exception => exception
-
DataMapper.logger.error(exception.to_s) if DataMapper.logger
-
raise
-
ensure
-
226
close_connection(connection)
-
end
-
-
# @api private
-
1
def select_fields(reader, fields)
-
fields = fields.map { |field| DataMapper::Inflector.underscore(field).to_sym }
-
struct = Struct.new(*fields)
-
-
results = []
-
-
while reader.next!
-
results << struct.new(*reader.values)
-
end
-
-
results
-
end
-
-
# @api private
-
1
def select_field(reader)
-
71
results = []
-
-
71
while reader.next!
-
131
results << reader.values.at(0)
-
end
-
-
71
results
-
end
-
-
# This module is just for organization. The methods are included into the
-
# Adapter below.
-
1
module SQL #:nodoc:
-
1
IDENTIFIER_MAX_LENGTH = 128
-
-
# @api semipublic
-
1
def property_to_column_name(property, qualify)
-
292
column_name = ''
-
-
292
case qualify
-
when true
-
column_name << "#{quote_name(property.model.storage_name(name))}."
-
when String
-
column_name << "#{quote_name(qualify)}."
-
end
-
-
292
column_name << quote_name(property.field)
-
end
-
-
1
private
-
-
# Adapters requiring a RETURNING syntax for INSERT statements
-
# should overwrite this to return true.
-
#
-
# @api private
-
1
def supports_returning?
-
false
-
end
-
-
# Adapters that do not support the DEFAULT VALUES syntax for
-
# INSERT statements should overwrite this to return false.
-
#
-
# @api private
-
1
def supports_default_values?
-
20
true
-
end
-
-
# Constructs SELECT statement for given query,
-
#
-
# @return [String] SELECT statement as a string
-
#
-
# @api private
-
1
def select_statement(query)
-
95
qualify = query.links.any?
-
95
fields = query.fields
-
95
order_by = query.order
-
95
group_by = if query.unique?
-
fields.select { |property| property.kind_of?(Property) }
-
end
-
-
95
conditions_statement, bind_values = conditions_statement(query.conditions, qualify)
-
-
95
statement = "SELECT #{columns_statement(fields, qualify)}"
-
95
statement << " FROM #{quote_name(query.model.storage_name(name))}"
-
95
statement << " #{join_statement(query, bind_values, qualify)}" if qualify
-
95
statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
-
95
statement << " GROUP BY #{columns_statement(group_by, qualify)}" if group_by && group_by.any?
-
95
statement << " ORDER BY #{order_statement(order_by, qualify)}" if order_by && order_by.any?
-
-
95
add_limit_offset!(statement, query.limit, query.offset, bind_values)
-
-
95
return statement, bind_values
-
end
-
-
# default construction of LIMIT and OFFSET
-
# overriden by some adapters (currently Oracle and SQL Server)
-
1
def add_limit_offset!(statement, limit, offset, bind_values)
-
95
if limit
-
83
statement << ' LIMIT ?'
-
83
bind_values << limit
-
end
-
-
95
if limit && offset > 0
-
statement << ' OFFSET ?'
-
bind_values << offset
-
end
-
end
-
-
# Constructs INSERT statement for given query,
-
#
-
# @return [String] INSERT statement as a string
-
#
-
# @api private
-
1
def insert_statement(model, properties, serial)
-
20
statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
-
-
20
if supports_default_values? && properties.empty?
-
statement << default_values_clause
-
else
-
statement << DataMapper::Ext::String.compress_lines(<<-SQL)
-
45
(#{properties.map { |property| quote_name(property.field) }.join(', ')})
-
VALUES
-
20
(#{(['?'] * properties.size).join(', ')})
-
20
SQL
-
end
-
-
20
if supports_returning? && serial
-
20
statement << returning_clause(serial)
-
end
-
-
20
statement
-
end
-
-
# by default PostgreSQL syntax
-
# overrided in Oracle adapter
-
1
def default_values_clause
-
'DEFAULT VALUES'
-
end
-
-
# by default PostgreSQL syntax
-
# overrided in Oracle adapter
-
1
def returning_clause(serial)
-
20
" RETURNING #{quote_name(serial.field)}"
-
end
-
-
# Constructs UPDATE statement for given query,
-
#
-
# @return [String] UPDATE statement as a string
-
#
-
# @api private
-
1
def update_statement(properties, query)
-
model = query.model
-
name = self.name
-
-
# TODO: DRY this up with delete_statement
-
conditions_statement, bind_values = if query.limit || query.links.any?
-
subquery(query, model.key(name), false)
-
else
-
conditions_statement(query.conditions)
-
end
-
-
statement = "UPDATE #{quote_name(model.storage_name(name))}"
-
statement << " SET #{properties.map { |property| "#{quote_name(property.field)} = ?" }.join(', ')}"
-
statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
-
-
return statement, bind_values
-
end
-
-
# Constructs DELETE statement for given query,
-
#
-
# @return [String] DELETE statement as a string
-
#
-
# @api private
-
1
def delete_statement(query)
-
model = query.model
-
name = self.name
-
-
# TODO: DRY this up with update_statement
-
conditions_statement, bind_values = if query.limit || query.links.any?
-
subquery(query, model.key(name), false)
-
else
-
conditions_statement(query.conditions)
-
end
-
-
statement = "DELETE FROM #{quote_name(model.storage_name(name))}"
-
statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
-
-
return statement, bind_values
-
end
-
-
# Constructs comma separated list of fields
-
#
-
# @return [String]
-
# list of fields as a string
-
#
-
# @api private
-
1
def columns_statement(properties, qualify)
-
246
properties.map { |property| property_to_column_name(property, qualify) }.join(', ')
-
end
-
-
# Constructs joins clause
-
#
-
# @return [String]
-
# joins clause
-
#
-
# @api private
-
1
def join_statement(query, bind_values, qualify)
-
statements = []
-
join_bind_values = []
-
-
target_alias = query.model.storage_name(name)
-
seen = { target_alias => 0 }
-
-
query.links.reverse_each do |relationship|
-
target_alias = relationship.target_model.storage_name(name)
-
storage_name = relationship.source_model.storage_name(name)
-
source_alias = storage_name
-
-
statements << "INNER JOIN #{quote_name(storage_name)}"
-
-
if seen.key?(source_alias)
-
seen[source_alias] += 1
-
source_alias = "#{source_alias}_#{seen[source_alias]}"
-
statements << quote_name(source_alias)
-
else
-
seen[source_alias] = 0
-
end
-
-
statements << 'ON'
-
-
add_join_conditions(relationship, target_alias, source_alias, statements)
-
add_extra_join_conditions(relationship, target_alias, statements, join_bind_values)
-
end
-
-
# prepend the join bind values to the statement bind values
-
bind_values.unshift(*join_bind_values)
-
-
statements.join(' ')
-
end
-
-
1
def add_join_conditions(relationship, target_alias, source_alias, statements)
-
statements << relationship.target_key.zip(relationship.source_key).map do |target_property, source_property|
-
"#{property_to_column_name(target_property, target_alias)} = #{property_to_column_name(source_property, source_alias)}"
-
end.join(' AND ')
-
end
-
-
1
def add_extra_join_conditions(relationship, target_alias, statements, bind_values)
-
conditions = DataMapper.repository(name).scope do
-
relationship.target_model.all(relationship.query).query.conditions
-
end
-
-
return if conditions.nil?
-
-
extra_statement, extra_bind_values = conditions_statement(conditions, target_alias)
-
statements << "AND #{extra_statement}"
-
bind_values.concat(extra_bind_values)
-
end
-
-
# Constructs where clause
-
#
-
# @return [String]
-
# where clause
-
#
-
# @api private
-
1
def conditions_statement(conditions, qualify = false)
-
177
case conditions
-
when Query::Conditions::NotOperation then negate_operation(conditions.operand, qualify)
-
95
when Query::Conditions::AbstractOperation then operation_statement(conditions, qualify)
-
82
when Query::Conditions::AbstractComparison then comparison_statement(conditions, qualify)
-
when Array
-
statement, bind_values = conditions # handle raw conditions
-
[ "(#{statement})", bind_values ].compact
-
end
-
end
-
-
# @api private
-
1
def supports_subquery?(*)
-
true
-
end
-
-
# @api private
-
1
def subquery(query, subject, qualify)
-
source_key, target_key = subquery_keys(subject)
-
-
if query.repository.name == name && supports_subquery?(query, source_key, target_key, qualify)
-
subquery_statement(query, source_key, target_key, qualify)
-
else
-
subquery_execute(query, source_key, target_key, qualify)
-
end
-
end
-
-
# @api private
-
1
def subquery_statement(query, source_key, target_key, qualify)
-
query = subquery_query(query, source_key)
-
select_statement, bind_values = select_statement(query)
-
-
statement = if target_key.size == 1
-
property_to_column_name(target_key.first, qualify)
-
else
-
"(#{target_key.map { |property| property_to_column_name(property, qualify) }.join(', ')})"
-
end
-
-
statement << " IN (#{select_statement})"
-
-
return statement, bind_values
-
end
-
-
# @api private
-
1
def subquery_execute(query, source_key, target_key, qualify)
-
query = subquery_query(query, source_key)
-
sources = query.model.all(query)
-
conditions = Query.target_conditions(sources, source_key, target_key)
-
-
if conditions.valid?
-
conditions_statement(conditions, qualify)
-
else
-
[ '1 = 0', [] ]
-
end
-
end
-
-
# @api private
-
1
def subquery_keys(subject)
-
case subject
-
when Associations::Relationship
-
relationship = subject.inverse
-
[ relationship.source_key, relationship.target_key ]
-
when PropertySet
-
[ subject, subject ]
-
end
-
end
-
-
# @api private
-
1
def subquery_query(query, source_key)
-
# force unique to be false because PostgreSQL has a problem with
-
# subselects that contain a GROUP BY with different columns
-
# than the outer-most query
-
query = query.merge(:fields => source_key, :unique => false)
-
query.update(:order => nil) unless query.limit
-
query
-
end
-
-
# Constructs order clause
-
#
-
# @return [String]
-
# order clause
-
#
-
# @api private
-
1
def order_statement(order, qualify)
-
71
statements = order.map do |direction|
-
71
statement = property_to_column_name(direction.target, qualify)
-
71
statement << ' DESC' if direction.operator == :desc
-
71
statement
-
end
-
-
71
statements.join(', ')
-
end
-
-
# @api private
-
1
def negate_operation(operand, qualify)
-
statement, bind_values = conditions_statement(operand, qualify)
-
statement = "NOT(#{statement})" unless statement.nil?
-
[ statement, bind_values ]
-
end
-
-
# @api private
-
1
def operation_statement(operation, qualify)
-
95
statements = []
-
95
bind_values = []
-
-
95
operation.each do |operand|
-
82
statement, values = conditions_statement(operand, qualify)
-
82
next unless statement
-
82
statements << statement
-
82
bind_values.concat(values) if values
-
end
-
-
95
statement = statements.join(" #{operation.slug.to_s.upcase} ")
-
-
95
if statements.size > 1
-
statement = "(#{statement})"
-
end
-
-
95
return statement, bind_values
-
end
-
-
# Constructs comparison clause
-
#
-
# @return [String]
-
# comparison clause
-
#
-
# @api private
-
1
def comparison_statement(comparison, qualify)
-
82
subject = comparison.subject
-
82
value = comparison.value
-
-
# TODO: move exclusive Range handling into another method, and
-
# update conditions_statement to use it
-
-
# break exclusive Range queries up into two comparisons ANDed together
-
82
if value.kind_of?(Range) && value.exclude_end?
-
operation = Query::Conditions::Operation.new(:and,
-
Query::Conditions::Comparison.new(:gte, subject, value.first),
-
Query::Conditions::Comparison.new(:lt, subject, value.last)
-
)
-
-
statement, bind_values = conditions_statement(operation, qualify)
-
-
return "(#{statement})", bind_values
-
82
elsif comparison.relationship?
-
if value.respond_to?(:query) && value.respond_to?(:loaded?) && !value.loaded?
-
return subquery(value.query, subject, qualify)
-
else
-
return conditions_statement(comparison.foreign_key_mapping, qualify)
-
end
-
82
elsif comparison.slug == :in && !value.any?
-
return [] # match everything
-
end
-
-
82
operator = comparison_operator(comparison)
-
82
column_name = property_to_column_name(subject, qualify)
-
-
# if operator return value contains ? then it means that it is function call
-
# and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
-
82
if operator.include?('?')
-
return operator % column_name, [ value ]
-
else
-
82
return "#{column_name} #{operator} #{value.nil? ? 'NULL' : '?'}", [ value ].compact
-
end
-
end
-
-
1
def comparison_operator(comparison)
-
82
subject = comparison.subject
-
82
value = comparison.value
-
-
82
case comparison.slug
-
82
when :eql then equality_operator(subject, value)
-
when :in then include_operator(subject, value)
-
when :regexp then regexp_operator(value)
-
when :like then like_operator(value)
-
when :gt then '>'
-
when :lt then '<'
-
when :gte then '>='
-
when :lte then '<='
-
end
-
end
-
-
# @api private
-
1
def equality_operator(property, operand)
-
82
operand.nil? ? 'IS' : '='
-
end
-
-
# @api private
-
1
def include_operator(property, operand)
-
case operand
-
when Array then 'IN'
-
when Range then 'BETWEEN'
-
end
-
end
-
-
# @api private
-
1
def regexp_operator(operand)
-
'~'
-
end
-
-
# @api private
-
1
def like_operator(operand)
-
'LIKE'
-
end
-
-
# @api private
-
1
def quote_name(name)
-
547
"\"#{name[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '""')}\""
-
end
-
-
end
-
-
1
include SQL
-
-
end
-
-
1
const_added(:DataObjectsAdapter)
-
end
-
end
-
1
require 'dm-core'
-
1
require 'dm-migrations/migration'
-
1
require 'dm-migrations/auto_migration'
-
1
require 'dm-migrations/auto_migration'
-
-
1
module DataMapper
-
1
module Migrations
-
-
1
module DataObjectsAdapter
-
-
# Returns whether the storage_name exists.
-
#
-
# @param [String] storage_name
-
# a String defining the name of a storage, for example a table name.
-
#
-
# @return [Boolean]
-
# true if the storage exists
-
#
-
# @api semipublic
-
1
def storage_exists?(storage_name)
-
5
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
-
SELECT COUNT(*)
-
FROM "information_schema"."tables"
-
WHERE "table_type" = 'BASE TABLE'
-
AND "table_schema" = ?
-
AND "table_name" = ?
-
SQL
-
-
5
select(statement, schema_name, storage_name).first > 0
-
end
-
-
# Returns whether the field exists.
-
#
-
# @param [String] storage_name
-
# a String defining the name of a storage, for example a table name.
-
# @param [String] field
-
# a String defining the name of a field, for example a column name.
-
#
-
# @return [Boolean]
-
# true if the field exists.
-
#
-
# @api semipublic
-
1
def field_exists?(storage_name, column_name)
-
20
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
-
SELECT COUNT(*)
-
FROM "information_schema"."columns"
-
WHERE "table_schema" = ?
-
AND "table_name" = ?
-
AND "column_name" = ?
-
SQL
-
-
20
select(statement, schema_name, storage_name, column_name).first > 0
-
end
-
-
# @api semipublic
-
1
def upgrade_model_storage(model)
-
5
name = self.name
-
5
properties = model.properties_with_subclasses(name)
-
-
5
if success = create_model_storage(model)
-
return properties
-
end
-
-
5
table_name = model.storage_name(name)
-
-
5
with_connection do |connection|
-
properties.map do |property|
-
20
schema_hash = property_schema_hash(property)
-
20
next if field_exists?(table_name, schema_hash[:name])
-
-
statement = alter_table_add_column_statement(connection, table_name, schema_hash)
-
command = connection.create_command(statement)
-
command.execute_non_query
-
-
# For simple :index => true columns, add an appropriate index.
-
# Upgrading doesn't know how to deal with complex indexes yet.
-
if property.options[:index] === true
-
statement = create_index_statement(model, property.name, [property.field])
-
command = connection.create_command(statement)
-
command.execute_non_query
-
end
-
-
property
-
5
end.compact
-
end
-
end
-
-
# @api semipublic
-
1
def create_model_storage(model)
-
5
name = self.name
-
5
properties = model.properties_with_subclasses(name)
-
-
5
return false if storage_exists?(model.storage_name(name))
-
return false if properties.empty?
-
-
with_connection do |connection|
-
statements = [ create_table_statement(connection, model, properties) ]
-
statements.concat(create_index_statements(model))
-
statements.concat(create_unique_index_statements(model))
-
-
statements.each do |statement|
-
command = connection.create_command(statement)
-
command.execute_non_query
-
end
-
end
-
-
true
-
end
-
-
# @api semipublic
-
1
def destroy_model_storage(model)
-
return true unless supports_drop_table_if_exists? || storage_exists?(model.storage_name(name))
-
execute(drop_table_statement(model))
-
true
-
end
-
-
1
module SQL #:nodoc:
-
# private ## This cannot be private for current migrations
-
-
# Adapters that support AUTO INCREMENT fields for CREATE TABLE
-
# statements should overwrite this to return true
-
#
-
# @api private
-
1
def supports_serial?
-
false
-
end
-
-
# @api private
-
1
def supports_drop_table_if_exists?
-
false
-
end
-
-
# @api private
-
1
def schema_name
-
raise NotImplementedError, "#{self.class}#schema_name not implemented"
-
end
-
-
# @api private
-
1
def alter_table_add_column_statement(connection, table_name, schema_hash)
-
"ALTER TABLE #{quote_name(table_name)} #{add_column_statement} #{property_schema_statement(connection, schema_hash)}"
-
end
-
-
# @api private
-
1
def create_table_statement(connection, model, properties)
-
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
-
CREATE TABLE #{quote_name(model.storage_name(name))}
-
(#{properties.map { |property| property_schema_statement(connection, property_schema_hash(property)) }.join(', ')},
-
PRIMARY KEY(#{ properties.key.map { |property| quote_name(property.field) }.join(', ')}))
-
SQL
-
-
statement
-
end
-
-
# @api private
-
1
def drop_table_statement(model)
-
table_name = quote_name(model.storage_name(name))
-
if supports_drop_table_if_exists?
-
"DROP TABLE IF EXISTS #{table_name}"
-
else
-
"DROP TABLE #{table_name}"
-
end
-
end
-
-
# @api private
-
1
def create_index_statements(model)
-
name = self.name
-
table_name = model.storage_name(name)
-
-
indexes(model).map do |index_name, fields|
-
create_index_statement(model, index_name, fields)
-
end
-
end
-
-
# @api private
-
1
def create_index_statement(model, index_name, fields)
-
table_name = model.storage_name(name)
-
-
DataMapper::Ext::String.compress_lines(<<-SQL)
-
CREATE INDEX #{quote_name("index_#{table_name}_#{index_name}")} ON
-
#{quote_name(table_name)} (#{fields.map { |field| quote_name(field) }.join(', ')})
-
SQL
-
end
-
-
# @api private
-
1
def create_unique_index_statements(model)
-
name = self.name
-
table_name = model.storage_name(name)
-
key = model.key(name).map { |property| property.field }
-
unique_indexes = unique_indexes(model).reject { |index_name, fields| fields == key }
-
-
unique_indexes.map do |index_name, fields|
-
DataMapper::Ext::String.compress_lines(<<-SQL)
-
CREATE UNIQUE INDEX #{quote_name("unique_#{table_name}_#{index_name}")} ON
-
#{quote_name(table_name)} (#{fields.map { |field| quote_name(field) }.join(', ')})
-
SQL
-
end
-
end
-
-
# @api private
-
1
def property_schema_hash(property)
-
20
primitive = property.primitive
-
20
type_map = self.class.type_map
-
-
20
schema = (type_map[property.class] || type_map[property.class.superclass] || type_map[primitive]).merge(:name => property.field)
-
-
20
schema_primitive = schema[:primitive]
-
-
20
if primitive == String && schema_primitive != 'TEXT' && schema_primitive != 'CLOB' && schema_primitive != 'NVARCHAR'
-
5
schema[:length] = property.length
-
15
elsif primitive == BigDecimal || primitive == Float
-
schema[:precision] = property.precision
-
schema[:scale] = property.scale
-
end
-
-
20
schema[:allow_nil] = property.allow_nil?
-
20
schema[:serial] = property.serial?
-
-
20
default = property.default
-
-
20
if default.nil? || default.respond_to?(:call)
-
# remove the default if the property does not allow nil
-
20
schema.delete(:default) unless schema[:allow_nil]
-
else
-
schema[:default] = property.dump(default)
-
end
-
-
20
schema
-
end
-
-
# @api private
-
1
def property_schema_statement(connection, schema)
-
statement = quote_name(schema[:name])
-
statement << " #{schema[:primitive]}"
-
-
length = schema[:length]
-
-
if schema[:precision] && schema[:scale]
-
statement << "(#{[ :precision, :scale ].map { |key| connection.quote_value(schema[key]) }.join(', ')})"
-
elsif length == 'max'
-
statement << '(max)'
-
elsif length
-
statement << "(#{connection.quote_value(length)})"
-
end
-
-
statement << " DEFAULT #{connection.quote_value(schema[:default])}" if schema.key?(:default)
-
statement << ' NOT NULL' unless schema[:allow_nil]
-
statement
-
end
-
-
# @api private
-
1
def indexes(model)
-
model.properties(name).indexes
-
end
-
-
# @api private
-
1
def unique_indexes(model)
-
model.properties(name).unique_indexes
-
end
-
-
# @api private
-
1
def add_column_statement
-
'ADD COLUMN'
-
end
-
end # module SQL
-
-
1
include SQL
-
-
1
module ClassMethods
-
# Default types for all data object based adapters.
-
#
-
# @return [Hash] default types for data objects adapters.
-
#
-
# @api private
-
1
def type_map
-
20
length = Property::String.length
-
20
precision = Property::Numeric.precision
-
20
scale = Property::Decimal.scale
-
-
{
-
Property::Binary => { :primitive => 'BLOB' },
-
Object => { :primitive => 'TEXT' },
-
Integer => { :primitive => 'INTEGER' },
-
String => { :primitive => 'VARCHAR', :length => length },
-
Class => { :primitive => 'VARCHAR', :length => length },
-
BigDecimal => { :primitive => 'DECIMAL', :precision => precision, :scale => scale },
-
Float => { :primitive => 'FLOAT', :precision => precision },
-
DateTime => { :primitive => 'TIMESTAMP' },
-
Date => { :primitive => 'DATE' },
-
Time => { :primitive => 'TIMESTAMP' },
-
TrueClass => { :primitive => 'BOOLEAN' },
-
Property::Text => { :primitive => 'TEXT' },
-
20
}.freeze
-
end
-
end
-
end
-
-
end
-
end
-
1
require 'dm-migrations/auto_migration'
-
1
require 'dm-migrations/adapters/dm-do-adapter'
-
-
1
module DataMapper
-
1
module Migrations
-
1
module PostgresAdapter
-
-
1
include DataObjectsAdapter
-
-
# @api private
-
1
def self.included(base)
-
1
base.extend DataObjectsAdapter::ClassMethods
-
1
base.extend ClassMethods
-
end
-
-
# @api semipublic
-
1
def upgrade_model_storage(model)
-
10
without_notices { super }
-
end
-
-
# @api semipublic
-
1
def create_model_storage(model)
-
10
without_notices { super }
-
end
-
-
# @api semipublic
-
1
def destroy_model_storage(model)
-
if supports_drop_table_if_exists?
-
without_notices { super }
-
else
-
super
-
end
-
end
-
-
1
module SQL #:nodoc:
-
# private ## This cannot be private for current migrations
-
-
# @api private
-
1
def supports_drop_table_if_exists?
-
@supports_drop_table_if_exists ||= postgres_version >= '8.2'
-
end
-
-
# @api private
-
1
def schema_name
-
25
@schema_name ||= select('SELECT current_schema()').first.freeze
-
end
-
-
# @api private
-
1
def postgres_version
-
@postgres_version ||= select('SELECT version()').first.split[1].freeze
-
end
-
-
# @api private
-
1
def without_notices
-
# execute the block with NOTICE messages disabled
-
10
begin
-
10
execute('SET client_min_messages = warning')
-
10
yield
-
ensure
-
10
execute('RESET client_min_messages')
-
end
-
end
-
-
# @api private
-
1
def property_schema_hash(property)
-
20
schema = super
-
-
20
primitive = property.primitive
-
-
# Postgres does not support precision and scale for Float
-
20
if primitive == Float
-
schema.delete(:precision)
-
schema.delete(:scale)
-
end
-
-
20
if property.kind_of?(Property::Integer)
-
11
min = property.min
-
11
max = property.max
-
-
11
schema[:primitive] = integer_column_statement(min..max) if min && max
-
end
-
-
20
if schema[:serial]
-
4
schema[:primitive] = serial_column_statement(min..max)
-
end
-
-
20
schema
-
end
-
-
1
private
-
-
# Return SQL statement for the integer column
-
#
-
# @param [Range] range
-
# the min/max allowed integers
-
#
-
# @return [String]
-
# the statement to create the integer column
-
#
-
# @api private
-
1
def integer_column_statement(range)
-
10
min = range.first
-
10
max = range.last
-
-
10
smallint = 2**15
-
10
integer = 2**31
-
10
bigint = 2**63
-
-
10
if min >= -smallint && max < smallint then 'SMALLINT'
-
10
elsif min >= -integer && max < integer then 'INTEGER'
-
elsif min >= -bigint && max < bigint then 'BIGINT'
-
else
-
raise ArgumentError, "min #{min} and max #{max} exceeds supported range"
-
end
-
end
-
-
# Return SQL statement for the serial column
-
#
-
# @param [Integer] max
-
# the max allowed integer
-
#
-
# @return [String]
-
# the statement to create the serial column
-
#
-
# @api private
-
1
def serial_column_statement(range)
-
4
max = range.last
-
-
4
if max.nil? || max < 2**31 then 'SERIAL'
-
elsif max < 2**63 then 'BIGSERIAL'
-
else
-
raise ArgumentError, "min #{range.first} and max #{max} exceeds supported range"
-
end
-
end
-
end # module SQL
-
-
1
include SQL
-
-
1
module ClassMethods
-
# Types for PostgreSQL databases.
-
#
-
# @return [Hash] types for PostgreSQL databases.
-
#
-
# @api private
-
1
def type_map
-
20
precision = Property::Numeric.precision
-
20
scale = Property::Decimal.scale
-
-
super.merge(
-
Property::Binary => { :primitive => 'BYTEA' },
-
BigDecimal => { :primitive => 'NUMERIC', :precision => precision, :scale => scale },
-
Float => { :primitive => 'DOUBLE PRECISION' }
-
20
).freeze
-
end
-
end
-
-
end
-
end
-
end
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
module Migrations
-
1
module SingletonMethods
-
-
# destructively migrates the repository upwards to match model definitions
-
#
-
# @param [Symbol] name repository to act on, :default is the default
-
#
-
# @api public
-
1
def migrate!(repository_name = nil)
-
repository(repository_name).migrate!
-
end
-
-
# drops and recreates the repository upwards to match model definitions
-
#
-
# @param [Symbol] name repository to act on, :default is the default
-
#
-
# @api public
-
1
def auto_migrate!(repository_name = nil)
-
repository_execute(:auto_migrate!, repository_name)
-
end
-
-
# @api public
-
1
def auto_upgrade!(repository_name = nil)
-
1
repository_execute(:auto_upgrade!, repository_name)
-
end
-
-
1
private
-
-
# @api semipublic
-
1
def auto_migrate_down!(repository_name)
-
repository_execute(:auto_migrate_down!, repository_name)
-
end
-
-
# @api semipublic
-
1
def auto_migrate_up!(repository_name)
-
repository_execute(:auto_migrate_up!, repository_name)
-
end
-
-
# @api private
-
1
def repository_execute(method, repository_name)
-
1
models = DataMapper::Model.descendants
-
1
models = models.select { |m| m.default_repository_name == repository_name } if repository_name
-
1
models.each do |model|
-
5
model.send(method, model.default_repository_name)
-
end
-
end
-
end
-
-
1
module Repository
-
# Determine whether a particular named storage exists in this repository
-
#
-
# @param [String]
-
# storage_name name of the storage to test for
-
#
-
# @return [Boolean]
-
# true if the data-store +storage_name+ exists
-
#
-
# @api semipublic
-
1
def storage_exists?(storage_name)
-
adapter = self.adapter
-
if adapter.respond_to?(:storage_exists?)
-
adapter.storage_exists?(storage_name)
-
end
-
end
-
-
# @api semipublic
-
1
def upgrade_model_storage(model)
-
5
adapter = self.adapter
-
5
if adapter.respond_to?(:upgrade_model_storage)
-
5
adapter.upgrade_model_storage(model)
-
end
-
end
-
-
# @api semipublic
-
1
def create_model_storage(model)
-
adapter = self.adapter
-
if adapter.respond_to?(:create_model_storage)
-
adapter.create_model_storage(model)
-
end
-
end
-
-
# @api semipublic
-
1
def destroy_model_storage(model)
-
adapter = self.adapter
-
if adapter.respond_to?(:destroy_model_storage)
-
adapter.destroy_model_storage(model)
-
end
-
end
-
-
# Destructively automigrates the data-store to match the model.
-
# First migrates all models down and then up.
-
# REPEAT: THIS IS DESTRUCTIVE
-
#
-
# @api public
-
1
def auto_migrate!
-
DataMapper.auto_migrate!(name)
-
end
-
-
# Safely migrates the data-store to match the model
-
# preserving data already in the data-store
-
#
-
# @api public
-
1
def auto_upgrade!
-
DataMapper.auto_upgrade!(name)
-
end
-
end # module Repository
-
-
1
module Model
-
-
# @api private
-
1
def self.included(mod)
-
1
mod.descendants.each { |model| model.extend self }
-
end
-
-
# @api semipublic
-
1
def storage_exists?(repository_name = default_repository_name)
-
repository(repository_name).storage_exists?(storage_name(repository_name))
-
end
-
-
# Destructively automigrates the data-store to match the model
-
# REPEAT: THIS IS DESTRUCTIVE
-
#
-
# @param Symbol repository_name the repository to be migrated
-
#
-
# @api public
-
1
def auto_migrate!(repository_name = self.repository_name)
-
assert_valid(true)
-
auto_migrate_down!(repository_name)
-
auto_migrate_up!(repository_name)
-
end
-
-
# Safely migrates the data-store to match the model
-
# preserving data already in the data-store
-
#
-
# @param Symbol repository_name the repository to be migrated
-
#
-
# @api public
-
1
def auto_upgrade!(repository_name = self.repository_name)
-
5
assert_valid(true)
-
5
base_model = self.base_model
-
5
if base_model == self
-
5
repository(repository_name).upgrade_model_storage(self)
-
else
-
base_model.auto_upgrade!(repository_name)
-
end
-
end
-
-
# Destructively migrates the data-store down, which basically
-
# deletes all the models.
-
# REPEAT: THIS IS DESTRUCTIVE
-
#
-
# @param Symbol repository_name the repository to be migrated
-
#
-
# @api private
-
1
def auto_migrate_down!(repository_name = self.repository_name)
-
assert_valid(true)
-
base_model = self.base_model
-
if base_model == self
-
repository(repository_name).destroy_model_storage(self)
-
else
-
base_model.auto_migrate_down!(repository_name)
-
end
-
end
-
-
# Auto migrates the data-store to match the model
-
#
-
# @param Symbol repository_name the repository to be migrated
-
#
-
# @api private
-
1
def auto_migrate_up!(repository_name = self.repository_name)
-
assert_valid(true)
-
base_model = self.base_model
-
if base_model == self
-
repository(repository_name).create_model_storage(self)
-
else
-
base_model.auto_migrate_up!(repository_name)
-
end
-
end
-
-
end # module Model
-
-
1
def self.include_migration_api
-
1
DataMapper.extend(SingletonMethods)
-
1
[ :Repository, :Model ].each do |name|
-
2
DataMapper.const_get(name).send(:include, const_get(name))
-
end
-
1
DataMapper::Model.append_extensions(Model)
-
1
Adapters::AbstractAdapter.descendants.each do |adapter_class|
-
Adapters.include_migration_api(DataMapper::Inflector.demodulize(adapter_class.name))
-
end
-
end
-
-
end
-
-
1
module Adapters
-
-
1
def self.include_migration_api(const_name)
-
2
require auto_migration_extensions(const_name)
-
2
if Migrations.const_defined?(const_name)
-
2
adapter = const_get(const_name)
-
2
adapter.send(:include, migration_module(const_name))
-
end
-
rescue LoadError
-
# Silently ignore the fact that no adapter extensions could be required
-
# This means that the adapter in use doesn't support migrations
-
end
-
-
1
def self.migration_module(const_name)
-
2
Migrations.const_get(const_name)
-
end
-
-
1
class << self
-
1
private
-
-
# @api private
-
1
def auto_migration_extensions(const_name)
-
2
name = adapter_name(const_name)
-
2
name = 'do' if name == 'dataobjects'
-
2
"dm-migrations/adapters/dm-#{name}-adapter"
-
end
-
-
end
-
-
1
extendable do
-
# @api private
-
1
def const_added(const_name)
-
2
include_migration_api(const_name)
-
2
super
-
end
-
end
-
-
end # module Adapters
-
-
1
Migrations.include_migration_api
-
-
end # module DataMapper
-
1
module DataMapper
-
1
module Migrations
-
1
class DuplicateMigration < StandardError
-
end
-
end
-
end
-
1
require 'dm-migrations/exceptions/duplicate_migration'
-
1
require 'dm-migrations/sql'
-
-
1
require 'benchmark'
-
-
1
module DataMapper
-
1
class Migration
-
1
include SQL
-
-
# The position or version the migration belongs to
-
1
attr_reader :position
-
-
# The name of the migration
-
1
attr_reader :name
-
-
# The repository the migration operates on
-
1
attr_reader :repository
-
-
#
-
# Creates a new migration.
-
#
-
# @param [Symbol, String, Integer] position
-
# The position or version the migration belongs to.
-
#
-
# @param [Symbol] name
-
# The name of the migration.
-
#
-
# @param [Hash] options
-
# Additional options for the migration.
-
#
-
# @option options [Boolean] :verbose (true)
-
# Enables or disables verbose output.
-
#
-
# @option options [Symbol] :repository (:default)
-
# The DataMapper repository the migration will operate on.
-
#
-
1
def initialize(position, name, options = {}, &block)
-
@position = position
-
@name = name
-
@options = options
-
@verbose = options.fetch(:verbose, true)
-
@up_action = nil
-
@down_action = nil
-
-
@repository = if options.key?(:database)
-
warn 'Using the :database option with migrations is deprecated, use :repository instead'
-
options[:database]
-
else
-
options.fetch(:repository, :default)
-
end
-
-
instance_eval(&block)
-
end
-
-
#
-
# The repository the migration will operate on.
-
#
-
# @return [Symbol, nil]
-
# The name of the DataMapper repository the migration will run against.
-
#
-
# @deprecated Use {#repository} instead.
-
#
-
# @since 1.0.1.
-
#
-
1
def database
-
warn "Using the DataMapper::Migration#database method is deprecated, use #repository instead"
-
@repository
-
end
-
-
#
-
# The adapter the migration will use.
-
#
-
# @return [DataMapper::Adapter]
-
# The adapter the migration will operate on.
-
#
-
# @since 1.0.1
-
#
-
1
def adapter
-
setup! unless setup?
-
-
@adapter
-
end
-
-
# define the actions that should be performed on an up migration
-
1
def up(&block)
-
@up_action = block
-
end
-
-
# define the actions that should be performed on a down migration
-
1
def down(&block)
-
@down_action = block
-
end
-
-
# perform the migration by running the code in the #up block
-
1
def perform_up
-
result = nil
-
-
if needs_up?
-
# TODO: fix this so it only does transactions for databases that support create/drop
-
# database.transaction.commit do
-
if @up_action
-
say_with_time "== Performing Up Migration ##{position}: #{name}", 0 do
-
result = @up_action.call
-
end
-
end
-
-
update_migration_info(:up)
-
# end
-
end
-
-
result
-
end
-
-
# un-do the migration by running the code in the #down block
-
1
def perform_down
-
result = nil
-
-
if needs_down?
-
# TODO: fix this so it only does transactions for databases that support create/drop
-
# database.transaction.commit do
-
if @down_action
-
say_with_time "== Performing Down Migration ##{position}: #{name}", 0 do
-
result = @down_action.call
-
end
-
end
-
-
update_migration_info(:down)
-
# end
-
end
-
-
result
-
end
-
-
# execute raw SQL
-
1
def execute(sql, *bind_values)
-
say_with_time(sql) do
-
adapter.execute(sql, *bind_values)
-
end
-
end
-
-
1
def create_table(table_name, opts = {}, &block)
-
execute TableCreator.new(adapter, table_name, opts, &block).to_sql
-
end
-
-
1
def drop_table(table_name, opts = {})
-
execute "DROP TABLE #{adapter.send(:quote_name, table_name.to_s)}"
-
end
-
-
1
def modify_table(table_name, opts = {}, &block)
-
TableModifier.new(adapter, table_name, opts, &block).statements.each do |sql|
-
execute(sql)
-
end
-
end
-
-
1
def create_index(table_name, *columns_and_options)
-
if columns_and_options.last.is_a?(Hash)
-
opts = columns_and_options.pop
-
else
-
opts = {}
-
end
-
columns = columns_and_options.flatten
-
-
opts[:name] ||= "#{opts[:unique] ? 'unique_' : ''}index_#{table_name}_#{columns.join('_')}"
-
-
execute DataMapper::Ext::String.compress_lines(<<-SQL)
-
CREATE #{opts[:unique] ? 'UNIQUE ' : '' }INDEX #{quote_column_name(opts[:name])} ON
-
#{quote_table_name(table_name)} (#{columns.map { |c| quote_column_name(c) }.join(', ') })
-
SQL
-
end
-
-
# Orders migrations by position, so we know what order to run them in.
-
# First order by position, then by name, so at least the order is predictable.
-
1
def <=> other
-
if self.position == other.position
-
self.name.to_s <=> other.name.to_s
-
else
-
self.position <=> other.position
-
end
-
end
-
-
# Output some text. Optional indent level
-
1
def say(message, indent = 4)
-
write "#{" " * indent} #{message}"
-
end
-
-
# Time how long the block takes to run, and output it with the message.
-
1
def say_with_time(message, indent = 2)
-
say(message, indent)
-
result = nil
-
time = Benchmark.measure { result = yield }
-
say("-> %.4fs" % time.real, indent)
-
result
-
end
-
-
# output the given text, but only if verbose mode is on
-
1
def write(text="")
-
puts text if @verbose
-
end
-
-
# Inserts or removes a row into the `migration_info` table, so we can mark this migration as run, or un-done
-
1
def update_migration_info(direction)
-
save, @verbose = @verbose, false
-
-
create_migration_info_table_if_needed
-
-
if direction.to_sym == :up
-
execute("INSERT INTO #{migration_info_table} (#{migration_name_column}) VALUES (#{quoted_name})")
-
elsif direction.to_sym == :down
-
execute("DELETE FROM #{migration_info_table} WHERE #{migration_name_column} = #{quoted_name}")
-
end
-
@verbose = save
-
end
-
-
1
def create_migration_info_table_if_needed
-
save, @verbose = @verbose, false
-
unless migration_info_table_exists?
-
execute("CREATE TABLE #{migration_info_table} (#{migration_name_column} VARCHAR(255) UNIQUE)")
-
end
-
@verbose = save
-
end
-
-
# Quote the name of the migration for use in SQL
-
1
def quoted_name
-
"'#{name}'"
-
end
-
-
1
def migration_info_table_exists?
-
adapter.storage_exists?('migration_info')
-
end
-
-
# Fetch the record for this migration out of the migration_info table
-
1
def migration_record
-
return [] unless migration_info_table_exists?
-
adapter.select("SELECT #{migration_name_column} FROM #{migration_info_table} WHERE #{migration_name_column} = #{quoted_name}")
-
end
-
-
# True if the migration needs to be run
-
1
def needs_up?
-
return true unless migration_info_table_exists?
-
migration_record.empty?
-
end
-
-
# True if the migration has already been run
-
1
def needs_down?
-
return false unless migration_info_table_exists?
-
! migration_record.empty?
-
end
-
-
# Quoted table name, for the adapter
-
1
def migration_info_table
-
@migration_info_table ||= quote_table_name('migration_info')
-
end
-
-
# Quoted `migration_name` column, for the adapter
-
1
def migration_name_column
-
@migration_name_column ||= quote_column_name('migration_name')
-
end
-
-
1
def quote_table_name(table_name)
-
# TODO: Fix this for 1.9 - can't use this hack to access a private method
-
adapter.send(:quote_name, table_name.to_s)
-
end
-
-
1
def quote_column_name(column_name)
-
# TODO: Fix this for 1.9 - can't use this hack to access a private method
-
adapter.send(:quote_name, column_name.to_s)
-
end
-
-
1
protected
-
-
#
-
# Determines whether the migration has been setup.
-
#
-
# @return [Boolean]
-
# Specifies whether the migration has been setup.
-
#
-
# @since 1.0.1
-
#
-
1
def setup?
-
!(@adapter.nil?)
-
end
-
-
#
-
# Sets up the migration.
-
#
-
# @since 1.0.1
-
#
-
1
def setup!
-
@adapter = DataMapper.repository(@repository).adapter
-
-
case @adapter.class.name
-
when /Sqlite/ then @adapter.extend(SQL::Sqlite)
-
when /Mysql/ then @adapter.extend(SQL::Mysql)
-
when /Postgres/ then @adapter.extend(SQL::Postgres)
-
else
-
raise(RuntimeError,"Unsupported Migration Adapter #{@adapter.class}",caller)
-
end
-
end
-
end
-
end
-
1
require 'dm-migrations/sql/table_creator'
-
1
require 'dm-migrations/sql/table_modifier'
-
1
require 'dm-migrations/sql/sqlite'
-
1
require 'dm-migrations/sql/mysql'
-
1
require 'dm-migrations/sql/postgres'
-
1
module SQL
-
1
class Column
-
1
attr_accessor :name, :type, :not_null, :default_value, :primary_key, :unique
-
end
-
end
-
1
require 'dm-migrations/sql/table'
-
-
1
module SQL
-
1
module Mysql
-
-
1
def supports_schema_transactions?
-
false
-
end
-
-
1
def table(table_name)
-
SQL::Mysql::Table.new(self, table_name)
-
end
-
-
1
def recreate_database
-
execute "DROP DATABASE #{schema_name}"
-
execute "CREATE DATABASE #{schema_name}"
-
execute "USE #{schema_name}"
-
end
-
-
1
def supports_serial?
-
true
-
end
-
-
1
def table_options(opts)
-
opt_engine = opts[:storage_engine] || storage_engine
-
opt_char_set = opts[:character_set] || character_set
-
opt_collation = opts[:collation] || collation
-
-
" ENGINE = #{opt_engine} CHARACTER SET #{opt_char_set} COLLATE #{opt_collation}"
-
end
-
-
1
def property_schema_statement(connection, schema)
-
if supports_serial? && schema[:serial]
-
statement = "#{schema[:quote_column_name]} SERIAL PRIMARY KEY"
-
else
-
super
-
end
-
end
-
-
1
def change_column_type_statement(name, column)
-
"ALTER TABLE #{quote_name(name)} MODIFY COLUMN #{column.to_sql}"
-
end
-
-
1
class Table
-
1
def initialize(adapter, table_name)
-
@columns = []
-
adapter.table_info(table_name).each do |col_struct|
-
@columns << SQL::Mysql::Column.new(col_struct)
-
end
-
end
-
end
-
-
1
class Column
-
1
def initialize(col_struct)
-
@name, @type, @default_value, @primary_key = col_struct.name, col_struct.type, col_struct.dflt_value, col_struct.pk
-
-
@not_null = col_struct.notnull == 0
-
end
-
end
-
end
-
end
-
1
module SQL
-
1
module Postgres
-
-
1
def supports_schema_transactions?
-
true
-
end
-
-
1
def table(table_name)
-
SQL::Postgres::Table.new(self, table_name)
-
end
-
-
1
def recreate_database
-
execute 'DROP SCHEMA IF EXISTS test CASCADE'
-
execute 'CREATE SCHEMA test'
-
execute 'SET search_path TO test'
-
end
-
-
1
def supports_serial?
-
true
-
end
-
-
1
def property_schema_statement(connection, schema)
-
if supports_serial? && schema[:serial]
-
statement = "#{schema[:quote_column_name]} SERIAL PRIMARY KEY"
-
else
-
statement = super
-
if schema.has_key?(:sequence_name)
-
statement << " DEFAULT nextval('#{schema[:sequence_name]}') NOT NULL"
-
end
-
statement
-
end
-
statement
-
end
-
-
1
def table_options(opts)
-
''
-
end
-
-
1
def change_column_type_statement(name, column)
-
"ALTER TABLE #{quote_name(name)} ALTER COLUMN #{column.to_sql}"
-
end
-
-
1
class Table < SQL::Table
-
1
def initialize(adapter, table_name)
-
@adapter, @name = adapter, table_name
-
@columns = []
-
adapter.query_table(table_name).each do |col_struct|
-
@columns << SQL::Postgres::Column.new(col_struct)
-
end
-
-
query_column_constraints
-
end
-
-
1
def query_column_constraints
-
@adapter.select(
-
"SELECT * FROM information_schema.table_constraints WHERE table_name='#{@name}' AND table_schema=current_schema()"
-
).each do |table_constraint|
-
@adapter.select(
-
"SELECT * FROM information_schema.constraint_column_usage WHERE constraint_name='#{table_constraint.constraint_name}' AND table_schema=current_schema()"
-
).each do |constrained_column|
-
@columns.each do |column|
-
if column.name == constrained_column.column_name
-
case table_constraint.constraint_type
-
when "UNIQUE" then column.unique = true
-
when "PRIMARY KEY" then column.primary_key = true
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
class Column < SQL::Column
-
1
def initialize(col_struct)
-
@name, @type, @default_value = col_struct.column_name, col_struct.data_type, col_struct.column_default
-
-
@not_null = col_struct.is_nullable != "YES"
-
end
-
end
-
end
-
end
-
1
require 'dm-migrations/sql/table'
-
-
1
require 'fileutils'
-
-
1
module SQL
-
1
module Sqlite
-
-
1
def supports_schema_transactions?
-
true
-
end
-
-
1
def table(table_name)
-
SQL::Sqlite::Table.new(self, table_name)
-
end
-
-
1
def recreate_database
-
DataMapper.logger.info "Dropping #{@uri.path}"
-
FileUtils.rm_f(@uri.path)
-
# do nothing, sqlite will automatically create the database file
-
end
-
-
1
def table_options(opts)
-
''
-
end
-
-
1
def supports_serial?
-
true
-
end
-
-
1
def change_column_type_statement(*args)
-
raise NotImplementedError
-
end
-
-
1
class Table < SQL::Table
-
1
def initialize(adapter, table_name)
-
@columns = []
-
adapter.table_info(table_name).each do |col_struct|
-
@columns << SQL::Sqlite::Column.new(col_struct)
-
end
-
end
-
end
-
-
1
class Column < SQL::Column
-
1
def initialize(col_struct)
-
@name, @type, @default_value, @primary_key = col_struct.name, col_struct.type, col_struct.dflt_value, col_struct.pk
-
-
@not_null = col_struct.notnull == 0
-
end
-
end
-
end
-
end
-
1
require 'dm-migrations/sql/column'
-
-
1
module SQL
-
1
class Table
-
1
attr_accessor :name, :columns
-
-
1
def to_s
-
name
-
end
-
-
1
def column(column_name)
-
@columns.select { |c| c.name == column_name.to_s }.first
-
end
-
end
-
end
-
1
require 'dm-core'
-
-
1
module SQL
-
1
class TableCreator
-
-
1
extend DataMapper::Property::Lookup
-
-
1
attr_accessor :table_name, :opts
-
-
1
def initialize(adapter, table_name, opts = {}, &block)
-
@adapter = adapter
-
@table_name = table_name.to_s
-
@opts = opts
-
-
@columns = []
-
-
self.instance_eval &block
-
end
-
-
1
def quoted_table_name
-
@adapter.send(:quote_name, table_name)
-
end
-
-
1
def column(name, type, opts = {})
-
@columns << Column.new(@adapter, name, type, opts)
-
end
-
-
1
def to_sql
-
"CREATE TABLE #{quoted_table_name} (#{@columns.map{ |c| c.to_sql }.join(', ')})#{@adapter.table_options(@opts)}"
-
end
-
-
# A helper for using the native NOW() SQL function in a default
-
1
def now
-
SqlExpr.new('NOW()')
-
end
-
-
# A helper for using the native UUID() SQL function in a default
-
1
def uuid
-
SqlExpr.new('UUID()')
-
end
-
-
1
class SqlExpr
-
1
attr_accessor :sql
-
1
def initialize(sql)
-
@sql = sql
-
end
-
-
1
def to_s
-
@sql.to_s
-
end
-
end
-
-
1
class Column
-
1
attr_accessor :name, :type
-
-
1
def initialize(adapter, name, type, opts = {})
-
@adapter = adapter
-
@name = name.to_s
-
@opts = opts
-
@type = build_type(type)
-
end
-
-
1
def to_sql
-
type
-
end
-
-
1
private
-
-
1
def build_type(type_class)
-
schema = { :name => @name, :quote_column_name => quoted_name }
-
-
[ :nullable, :nullable? ].each do |option|
-
next if (value = schema.delete(option)).nil?
-
warn "#{option.inspect} is deprecated, use :allow_nil instead"
-
schema[:allow_nil] = value unless schema.key?(:allow_nil)
-
end
-
-
unless schema.key?(:allow_nil)
-
schema[:allow_nil] = !schema[:not_null]
-
end
-
-
if type_class.kind_of?(String)
-
schema[:primitive] = type_class
-
else
-
type_map = @adapter.class.type_map
-
primitive = type_class.respond_to?(:primitive) ? type_class.primitive : type_class
-
options = (type_map[type_class] || type_map[primitive])
-
-
schema.update(type_class.options) if type_class.respond_to?(:options)
-
schema.update(options)
-
-
schema.delete(:length) if type_class == DataMapper::Property::Text
-
end
-
-
schema.update(@opts)
-
-
schema[:length] = schema.delete(:size) if schema.key?(:size)
-
-
@adapter.send(:with_connection) do |connection|
-
@adapter.property_schema_statement(connection, schema)
-
end
-
end
-
-
1
def quoted_name
-
@adapter.send(:quote_name, name)
-
end
-
end
-
end
-
end
-
1
module SQL
-
1
class TableModifier
-
1
extend DataMapper::Property::Lookup
-
-
1
attr_accessor :table_name, :opts, :statements, :adapter
-
-
1
def initialize(adapter, table_name, opts = {}, &block)
-
@adapter = adapter
-
@table_name = table_name.to_s
-
@opts = (opts)
-
-
@statements = []
-
-
self.instance_eval &block
-
end
-
-
1
def add_column(name, type, opts = {})
-
column = SQL::TableCreator::Column.new(@adapter, name, type, opts)
-
@statements << "ALTER TABLE #{quoted_table_name} ADD COLUMN #{column.to_sql}"
-
end
-
-
1
def drop_column(name)
-
# raise NotImplemented for SQLite3. Can't ALTER TABLE, need to copy table.
-
# We'd have to inspect it, and we can't, since we aren't executing any queries yet.
-
# TODO instead of building the SQL queries when executing the block, create AddColumn,
-
# AlterColumn and DropColumn objects that get #to_sql'd
-
if name.is_a?(Array)
-
name.each{ |n| drop_column(n) }
-
else
-
@statements << "ALTER TABLE #{quoted_table_name} DROP COLUMN #{quote_column_name(name)}"
-
end
-
end
-
1
alias_method :drop_columns, :drop_column
-
-
1
def rename_column(name, new_name, opts = {})
-
# raise NotImplemented for SQLite3
-
@statements << "ALTER TABLE #{quoted_table_name} RENAME COLUMN #{quote_column_name(name)} TO #{quote_column_name(new_name)}"
-
end
-
-
1
def change_column(name, type, opts = {})
-
column = SQL::TableCreator::Column.new(@adapter, name, type, opts)
-
@statements << @adapter.change_column_type_statement(table_name, column)
-
end
-
-
1
def quote_column_name(name)
-
@adapter.send(:quote_name, name.to_s)
-
end
-
-
1
def quoted_table_name
-
@adapter.send(:quote_name, table_name)
-
end
-
-
1
def to_sql
-
@statements.join(';')
-
end
-
end
-
end
-
1
require 'dm-postgres-adapter/adapter'
-
1
require 'do_postgres'
-
1
require 'dm-do-adapter'
-
-
1
module DataMapper
-
1
module Adapters
-
-
1
class PostgresAdapter < DataObjectsAdapter
-
-
1
module SQL #:nodoc:
-
1
private
-
-
# @api private
-
1
def supports_returning?
-
20
true
-
end
-
end
-
-
1
include SQL
-
-
end
-
-
1
const_added(:PostgresAdapter)
-
-
end
-
end
-
1
require 'dm-serializer/to_json'
-
1
require 'dm-serializer/to_xml'
-
1
require 'dm-serializer/to_yaml'
-
1
require 'dm-serializer/to_csv'
-
-
1
module DataMapper
-
# Define the `Serialize` constant for backwards compatibility.
-
#
-
# @note
-
# The `Serialize` constant will be removed soon, please use
-
# {Serializer} instead.
-
#
-
1
Serialize = Serializer
-
end
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
module Serializer
-
-
# Returns propreties to serialize based on :only or :exclude arrays,
-
# if provided :only takes precendence over :exclude
-
#
-
# @return [Array]
-
# Properties that need to be serialized.
-
1
def properties_to_serialize(options)
-
only_properties = Array(options[:only])
-
excluded_properties = Array(options[:exclude])
-
-
model.properties(repository.name).reject do |p|
-
if only_properties.include? p.name
-
false
-
else
-
excluded_properties.include?(p.name) ||
-
!(only_properties.empty? ||
-
only_properties.include?(p.name))
-
end
-
end
-
end
-
end
-
-
1
Model.append_inclusions(Serializer)
-
end
-
1
require 'dm-serializer/common'
-
-
1
if RUBY_VERSION >= '1.9.0'
-
1
require 'csv'
-
else
-
begin
-
require 'fastercsv'
-
CSV = FasterCSV
-
rescue LoadError
-
# do nothing
-
end
-
end
-
-
1
module DataMapper
-
1
module Serializer
-
# Serialize a Resource to comma-separated values (CSV).
-
#
-
# @return <String> a CSV representation of the Resource
-
1
def to_csv(*args)
-
options = args.first || {}
-
options = options.to_h if options.respond_to?(:to_h)
-
options[:writer] = '' unless options.has_key? :writer
-
-
CSV.generate(options[:writer]) do |csv|
-
row = properties_to_serialize(options).map do |property|
-
__send__(property.name).to_s
-
end
-
csv << row
-
end
-
end
-
-
1
module ValidationErrors
-
1
module ToCsv
-
1
def to_csv(*args)
-
options = args.first || {}
-
options = options.to_h if options.respond_to?(:to_h)
-
options[:writer] = '' unless options.has_key? :writer
-
-
CSV.generate(options[:writer]) do |csv|
-
errors.each do |key, value|
-
value.each do |error|
-
row = []
-
row << key.to_s
-
row << error.to_s
-
csv << row
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
class Collection
-
1
def to_csv(*args)
-
result = ''
-
each do |item|
-
result << item.to_csv(args.first) + "\n"
-
end
-
result
-
end
-
end
-
-
1
if const_defined?(:Validations)
-
module Validations
-
class ValidationErrors
-
include DataMapper::Serializer::ValidationErrors::ToCsv
-
end
-
end
-
end
-
end
-
1
require 'dm-serializer/common'
-
-
1
require 'multi_json'
-
-
1
module DataMapper
-
1
module Serializer
-
#
-
# Converts the resource into a hash of properties.
-
#
-
# @param [Hash] options
-
# Additional options.
-
#
-
# @return [Hash{String => String}]
-
# The hash of resources properties.
-
#
-
# @since 1.0.1
-
#
-
1
def as_json(options = {})
-
options = {} if options.nil?
-
result = {}
-
-
properties_to_serialize(options).each do |property|
-
property_name = property.name
-
value = __send__(property_name)
-
result[property_name] = value.kind_of?(DataMapper::Model) ? value.name : value
-
end
-
-
# add methods
-
Array(options[:methods]).each do |method|
-
next unless respond_to?(method)
-
result[method] = __send__(method)
-
end
-
-
# Note: if you want to include a whole other model via relation, use
-
# :methods:
-
#
-
# comments.to_json(:relationships=>{:user=>{:include=>[:first_name],:methods=>[:age]}})
-
#
-
# TODO: This needs tests and also needs to be ported to #to_xml and
-
# #to_yaml
-
if options[:relationships]
-
options[:relationships].each do |relationship_name, opts|
-
if respond_to?(relationship_name)
-
result[relationship_name] = __send__(relationship_name).to_json(opts.merge(:to_json => false))
-
end
-
end
-
end
-
-
result
-
end
-
-
# Serialize a Resource to JavaScript Object Notation (JSON; RFC 4627)
-
#
-
# @return <String> a JSON representation of the Resource
-
1
def to_json(*args)
-
options = args.first
-
options = {} unless options.kind_of?(Hash)
-
-
result = as_json(options)
-
-
# default to making JSON
-
if options.fetch(:to_json, true)
-
MultiJson.encode(result)
-
else
-
result
-
end
-
end
-
-
1
module ValidationErrors
-
1
module ToJson
-
1
def to_json(*args)
-
MultiJson.encode(Hash[ errors ])
-
end
-
end
-
end
-
-
end
-
-
1
class Collection
-
1
def to_json(*args)
-
options = args.first
-
options = {} unless options.kind_of?(Hash)
-
-
resource_options = options.merge(:to_json => false)
-
collection = map { |resource| resource.to_json(resource_options) }
-
-
# default to making JSON
-
if options.fetch(:to_json, true)
-
MultiJson.encode(collection)
-
else
-
collection
-
end
-
end
-
end
-
-
1
if const_defined?(:Validations)
-
module Validations
-
class ValidationErrors
-
include DataMapper::Serializer::ValidationErrors::ToJson
-
end
-
end
-
end
-
end
-
1
require 'dm-serializer/common'
-
1
require 'dm-serializer/xml'
-
-
1
module DataMapper
-
1
module Serializer
-
# Serialize a Resource to XML.
-
#
-
# @return [LibXML::Document, Nokogiri::Document, REXML::Document]
-
# An XML representation of this Resource.
-
#
-
1
def to_xml(opts = {})
-
xml = XML.serializer
-
xml.output(to_xml_document(opts)).to_s
-
end
-
-
# This method requires certain methods to be implemented in the
-
# individual serializer library subclasses:
-
#
-
# * new_document
-
# * root_node
-
# * add_property_node
-
# * add_node
-
1
def to_xml_document(opts={}, doc = nil)
-
xml = XML.serializer
-
doc ||= xml.new_document
-
-
default_xml_element_name = lambda {
-
DataMapper::Inflector.underscore(model.name).tr("/", "-")
-
}
-
-
root = xml.root_node(
-
doc,
-
(opts[:element_name] || default_xml_element_name[])
-
)
-
-
properties_to_serialize(opts).each do |property|
-
value = __send__(property.name)
-
attrs = {}
-
-
unless property.primitive == String
-
attrs['type'] = property.primitive.to_s.downcase
-
end
-
-
xml.add_node(root, property.name.to_s, value, attrs)
-
end
-
-
Array(opts[:methods]).each do |meth|
-
if self.respond_to?(meth)
-
xml_name = meth.to_s.gsub(/[^a-z0-9_]/, '')
-
value = __send__(meth)
-
-
unless value.nil?
-
if value.respond_to?(:to_xml_document)
-
xml.add_xml(root, value.to_xml_document)
-
else
-
xml.add_node(root, xml_name, value.to_s)
-
end
-
end
-
end
-
end
-
-
doc
-
end
-
-
1
module ValidationErrors
-
1
module ToXml
-
1
def to_xml(opts = {})
-
to_xml_document(opts).to_s
-
end
-
-
1
def to_xml_document(opts = {})
-
xml = DataMapper::Serializer::XML.serializer
-
doc = xml.new_document
-
root = xml.root_node(doc, "errors", {'type' => 'hash'})
-
-
errors.each do |key, value|
-
property = xml.add_node(root, key.to_s, nil, {'type' => 'array'})
-
property.attributes["type"] = 'array'
-
-
value.each do |error|
-
xml.add_node(property, "error", error)
-
end
-
end
-
-
doc
-
end
-
end
-
end
-
-
end
-
-
1
class Collection
-
1
def to_xml(opts = {})
-
to_xml_document(opts).to_s
-
end
-
-
1
def to_xml_document(opts = {})
-
xml = DataMapper::Serializer::XML.serializer
-
doc = xml.new_document
-
-
default_collection_element_name = lambda {
-
DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(self.model.to_s)).tr("/", "-")
-
}
-
-
root = xml.root_node(
-
doc,
-
opts[:collection_element_name] || default_collection_element_name[],
-
{'type' => 'array'}
-
)
-
-
self.each do |item|
-
item.to_xml_document(opts, doc)
-
end
-
-
doc
-
end
-
end
-
-
1
if const_defined?(:Validations)
-
module Validations
-
class ValidationErrors
-
include DataMapper::Serializer::ValidationErrors::ToXml
-
end
-
end
-
end
-
end
-
1
require 'dm-serializer/common'
-
-
1
module DataMapper
-
1
module Serializer
-
1
TAG_NAME = "ruby/DataMapper,#{DataMapper::VERSION}".freeze
-
-
# Include a callback to register the YAML output
-
#
-
# @param [DataMapper::Model] descendant
-
#
-
# @return [undefined]
-
#
-
# @api private
-
1
def self.included(descendant)
-
5
YAML.add_domain_type(TAG_NAME, descendant.name) do |_tag, values|
-
values
-
end
-
end
-
-
# Serialize a Resource to YAML
-
#
-
# @example
-
# yaml = resource.to_yaml # => a valid YAML string
-
#
-
# @param [Hash] options
-
#
-
# @return [String]
-
#
-
# @api public
-
def to_yaml(options = {})
-
YAML.quick_emit(object_id, options) do |out|
-
out.map(to_yaml_type, to_yaml_style) do |map|
-
encode_with(map, options.kind_of?(Hash) ? options : {})
-
end
-
end
-
1
end unless YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck?
-
-
# A callback to encode the resource in the YAML stream
-
#
-
# @param [#add] coder
-
# handles adding the values to the output
-
#
-
# @param [Hash] options
-
# optional Hash configuring the output
-
#
-
# @return [undefined]
-
#
-
# @api public
-
1
def encode_with(coder, options = {})
-
coder.tag = to_yaml_type if coder.respond_to?(:tag=)
-
coder.style = to_yaml_style if coder.respond_to?(:style=)
-
-
methods = []
-
-
methods.concat properties_to_serialize(options).map { |property| property.name }
-
methods.concat Array(options[:methods])
-
-
methods.each do |method|
-
coder.add(method.to_s, __send__(method))
-
end
-
end
-
-
1
private
-
-
# Return the YAML type to use for the output
-
#
-
# @return [String]
-
#
-
# @api private
-
1
def to_yaml_type
-
"!#{TAG_NAME}:#{model.name}"
-
end
-
-
# Return the YAML style to use for the output
-
#
-
# @return [Integer]
-
#
-
# @api private
-
def to_yaml_style
-
Psych::Nodes::Mapping::ANY
-
1
end if YAML.const_defined?(:ENGINE) && YAML::ENGINE.yamler == 'psych'
-
-
1
module ValidationErrors
-
1
module ToYaml
-
-
# Serialize the errors to YAML
-
#
-
# @example
-
# yaml = errors.to_yaml # => a valid YAML string
-
#
-
# @param [Hash] options
-
#
-
# @return [String]
-
#
-
# @api public
-
1
def to_yaml(*args)
-
Hash[errors].to_yaml(*args)
-
end
-
-
# A callback to encode the errors in the YAML stream
-
#
-
# @param [#add] coder
-
# handles adding the values to the output
-
#
-
# @return [undefined]
-
#
-
# @api public
-
1
def encode_with(coder)
-
coder.map = Hash[errors]
-
end
-
-
end # module ToYaml
-
end # module ValidationErrors
-
-
1
module Collection
-
1
module ToYaml
-
-
# Serialize the collection to YAML
-
#
-
# @example
-
# yaml = collection.to_yaml # => a valid YAML string
-
#
-
# @param [Hash] options
-
#
-
# @return [String]
-
#
-
# @api public
-
1
def to_yaml(*args)
-
to_a.to_yaml(*args)
-
end
-
-
# A callback to encode the collection in the YAML stream
-
#
-
# @param [#add] coder
-
# handles adding the values to the output
-
#
-
# @return [undefined]
-
#
-
# @api public
-
1
def encode_with(coder)
-
coder.seq = to_a
-
end
-
-
end # module ToYaml
-
end # module Collection
-
end # module Serializer
-
-
1
class Collection
-
1
include Serializer::Collection::ToYaml
-
end # class Collection
-
-
1
if const_defined?(:Validations)
-
module Validations
-
class ValidationErrors
-
include DataMapper::Serializer::ValidationErrors::ToYaml
-
end # class ValidationErrors
-
end # module Validations
-
end
-
-
end # module DataMapper
-
1
module DataMapper
-
1
module Serializer
-
1
module XML
-
# The supported XML Serializers
-
1
SERIALIZERS = {
-
:libxml => 'LibXML',
-
:nokogiri => 'Nokogiri',
-
:rexml => 'REXML'
-
}
-
-
#
-
# The current XML Serializer.
-
#
-
# @return [Module]
-
# The module within {DataMapper::Serialize::XML}.
-
#
-
# @since 1.1.0
-
#
-
1
def self.serializer
-
@serializer
-
end
-
-
#
-
# Sets the XML Serializer to use.
-
#
-
# @param [Symbol] name
-
# The name of the serializer to use. Must be either `:libxml`,
-
# `:nokogiri` or `:rexml`.
-
#
-
# @return [Module]
-
# The module within {DataMapper::Serialize::XML}.
-
#
-
# @since 1.1.0
-
#
-
1
def self.serializer=(name)
-
1
serializer_const = SERIALIZERS[name]
-
-
1
unless serializer_const
-
raise(ArgumentError,"unsupported XML Serializer #{name}")
-
end
-
-
1
require "dm-serializer/xml/#{name}"
-
1
@serializer = const_get(serializer_const)
-
end
-
-
1
[:nokogiri, :libxml, :rexml].each do |name|
-
# attempt to load the first available XML Serializer
-
1
begin
-
1
self.serializer = name
-
1
break
-
rescue LoadError
-
end
-
end
-
end
-
end
-
end
-
1
require 'nokogiri'
-
-
1
module DataMapper
-
1
module Serializer
-
1
module XML
-
1
module Nokogiri
-
1
def self.new_document
-
::Nokogiri::XML::Document.new
-
end
-
-
1
def self.root_node(doc, name, attrs = {})
-
root = ::Nokogiri::XML::Node.new(name, doc)
-
-
attrs.each do |attr_name, attr_val|
-
root[attr_name] = attr_val
-
end
-
-
doc.root.nil? ? doc.root = root : doc.root << root
-
root
-
end
-
-
1
def self.add_node(parent, name, value, attrs = {})
-
node = ::Nokogiri::XML::Node.new(name, parent.document)
-
node << ::Nokogiri::XML::Text.new(value.to_s, parent.document) unless value.nil?
-
-
attrs.each do |attr_name, attr_val|
-
node[attr_name] = attr_val
-
end
-
-
parent << node
-
node
-
end
-
-
1
def self.add_xml(parent, xml)
-
parent << xml.root
-
end
-
-
1
def self.output(doc)
-
doc.root.to_s
-
end
-
end
-
end
-
end
-
end
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
module Timestamps
-
1
TIMESTAMP_PROPERTIES = {
-
:updated_at => [ DateTime, lambda { |r| DateTime.now } ],
-
:updated_on => [ Date, lambda { |r| Date.today } ],
-
:created_at => [ DateTime, lambda { |r| r.created_at || (DateTime.now if r.new?) } ],
-
:created_on => [ Date, lambda { |r| r.created_on || (Date.today if r.new?) } ],
-
}.freeze
-
-
1
def self.included(model)
-
5
model.before :save, :set_timestamps_on_save
-
5
model.extend ClassMethods
-
end
-
-
# Saves the record with the updated_at/on attributes set to the current time.
-
1
def touch
-
set_timestamps
-
save
-
end
-
-
1
private
-
-
1
def set_timestamps_on_save
-
20
return unless dirty?
-
20
set_timestamps
-
end
-
-
1
def set_timestamps
-
20
TIMESTAMP_PROPERTIES.each do |name,(_type,proc)|
-
80
if properties.named?(name)
-
attribute_set(name, proc.call(self))
-
end
-
end
-
end
-
-
1
module ClassMethods
-
1
def timestamps(*names)
-
raise ArgumentError, 'You need to pass at least one argument' if names.empty?
-
-
names.each do |name|
-
case name
-
when *TIMESTAMP_PROPERTIES.keys
-
options = { :required => true }
-
-
if Property.accepted_options.include?(:auto_validation)
-
options.update(:auto_validation => false)
-
end
-
-
property name, TIMESTAMP_PROPERTIES[name].first, options
-
when :at
-
timestamps(:created_at, :updated_at)
-
when :on
-
timestamps(:created_on, :updated_on)
-
else
-
raise InvalidTimestampName, "Invalid timestamp property name '#{name}'"
-
end
-
end
-
end
-
end # module ClassMethods
-
-
1
class InvalidTimestampName < RuntimeError; end
-
-
1
Model.append_inclusions self
-
end # module Timestamp
-
-
# include Timestamp or Timestamps, it still works
-
1
Timestamp = Timestamps
-
end # module DataMapper
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
class Transaction
-
1
extend Chainable
-
-
# @api private
-
1
attr_accessor :state
-
-
# @api private
-
1
def none?
-
state == :none
-
end
-
-
# @api private
-
1
def begin?
-
state == :begin
-
end
-
-
# @api private
-
1
def rollback?
-
state == :rollback
-
end
-
-
# @api private
-
1
def commit?
-
state == :commit
-
end
-
-
# Create a new Transaction
-
#
-
# @see Transaction#link
-
#
-
# In fact, it just calls #link with the given arguments at the end of the
-
# constructor.
-
#
-
# @api public
-
1
def initialize(*things)
-
@transaction_primitives = {}
-
self.state = :none
-
@adapters = {}
-
link(*things)
-
if block_given?
-
warn "Passing block to #{self.class.name}.new is deprecated (#{caller[0]})"
-
commit { |*block_args| yield(*block_args) }
-
end
-
end
-
-
# Associate this Transaction with some things.
-
#
-
# @param [Object] things
-
# the things you want this Transaction associated with:
-
#
-
# Adapters::AbstractAdapter subclasses will be added as
-
# adapters as is.
-
# Arrays will have their elements added.
-
# Repository will have it's own @adapters added.
-
# Resource subclasses will have all the repositories of all
-
# their properties added.
-
# Resource instances will have all repositories of all their
-
# properties added.
-
#
-
# @param [Proc] block
-
# a block (taking one argument, the Transaction) to execute within
-
# this transaction. The transaction will begin and commit around
-
# the block, and rollback if an exception is raised.
-
#
-
# @api private
-
1
def link(*things)
-
unless none?
-
raise "Illegal state for link: #{state}"
-
end
-
-
things.each do |thing|
-
case thing
-
when DataMapper::Adapters::AbstractAdapter
-
@adapters[thing] = :none
-
when DataMapper::Repository
-
link(thing.adapter)
-
when DataMapper::Model
-
link(*thing.repositories)
-
when DataMapper::Resource
-
link(thing.model)
-
when Array
-
link(*thing)
-
else
-
raise "Unknown argument to #{self.class}#link: #{thing.inspect} (#{thing.class})"
-
end
-
end
-
-
if block_given?
-
commit { |*block_args| yield(*block_args) }
-
else
-
self
-
end
-
end
-
-
# Begin the transaction
-
#
-
# Before #begin is called, the transaction is not valid and can not be used.
-
#
-
# @api private
-
1
def begin
-
unless none?
-
raise "Illegal state for begin: #{state}"
-
end
-
-
each_adapter(:connect_adapter, [:log_fatal_transaction_breakage])
-
each_adapter(:begin_adapter, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
-
self.state = :begin
-
end
-
-
# Commit the transaction
-
#
-
# If no block is given, it will simply commit any changes made since the
-
# Transaction did #begin.
-
#
-
# @param block<Block> a block (taking the one argument, the Transaction) to
-
# execute within this transaction. The transaction will begin and commit
-
# around the block, and roll back if an exception is raised.
-
#
-
# @api private
-
1
def commit
-
if block_given?
-
unless none?
-
raise "Illegal state for commit with block: #{state}"
-
end
-
-
begin
-
self.begin
-
rval = within { |*block_args| yield(*block_args) }
-
rescue Exception => exception
-
if begin?
-
rollback
-
end
-
raise exception
-
ensure
-
unless exception
-
if begin?
-
commit
-
end
-
return rval
-
end
-
end
-
else
-
unless begin?
-
raise "Illegal state for commit without block: #{state}"
-
end
-
each_adapter(:commit_adapter, [:log_fatal_transaction_breakage])
-
each_adapter(:close_adapter, [:log_fatal_transaction_breakage])
-
self.state = :commit
-
end
-
end
-
-
# Rollback the transaction
-
#
-
# Will undo all changes made during the transaction.
-
#
-
# @api private
-
1
def rollback
-
unless begin?
-
raise "Illegal state for rollback: #{state}"
-
end
-
each_adapter(:rollback_adapter_if_begin, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
-
each_adapter(:close_adapter_if_open, [:log_fatal_transaction_breakage])
-
self.state = :rollback
-
end
-
-
# Execute a block within this Transaction.
-
#
-
# No #begin, #commit or #rollback is performed in #within, but this
-
# Transaction will pushed on the per thread stack of transactions for each
-
# adapter it is associated with, and it will ensures that it will pop the
-
# Transaction away again after the block is finished.
-
#
-
# @param block<Block> the block of code to execute.
-
#
-
# @api private
-
1
def within
-
unless block_given?
-
raise 'No block provided'
-
end
-
-
unless begin?
-
raise "Illegal state for within: #{state}"
-
end
-
-
adapters = @adapters
-
-
adapters.each_key do |adapter|
-
adapter.push_transaction(self)
-
end
-
-
begin
-
yield self
-
ensure
-
adapters.each_key do |adapter|
-
adapter.pop_transaction
-
end
-
end
-
end
-
-
# @api private
-
1
def method_missing(method, *args, &block)
-
first_arg = args.first
-
-
return super unless args.size == 1 && first_arg.kind_of?(Adapters::AbstractAdapter)
-
return super unless match = method.to_s.match(/\A(.*)_(if|unless)_(none|begin|rollback|commit)\z/)
-
-
action, condition, expected_state = match.captures
-
return super unless respond_to?(action, true)
-
-
state = state_for(first_arg).to_s
-
execute = (condition == 'if') == (state == expected_state)
-
-
send(action, first_arg) if execute
-
end
-
-
# @api private
-
1
def primitive_for(adapter)
-
unless @adapters.include?(adapter)
-
raise "Unknown adapter #{adapter}"
-
end
-
-
unless @transaction_primitives.include?(adapter)
-
raise "No primitive for #{adapter}"
-
end
-
-
@transaction_primitives[adapter]
-
end
-
-
1
private
-
-
# @api private
-
1
def validate_primitive(primitive)
-
[:close, :begin, :rollback, :commit].each do |meth|
-
unless primitive.respond_to?(meth)
-
raise "Invalid primitive #{primitive}: doesnt respond_to?(#{meth.inspect})"
-
end
-
end
-
-
primitive
-
end
-
-
# @api private
-
1
def each_adapter(method, on_fail)
-
adapters = @adapters
-
begin
-
adapters.each_key do |adapter|
-
send(method, adapter)
-
end
-
rescue Exception => exception
-
adapters.each_key do |adapter|
-
on_fail.each do |fail_handler|
-
begin
-
send(fail_handler, adapter)
-
rescue Exception => inner_exception
-
DataMapper.logger.fatal("#{self}#each_adapter(#{method.inspect}, #{on_fail.inspect}) failed with #{exception.inspect}: #{exception.backtrace.join("\n")} - and when sending #{fail_handler} to #{adapter} we failed again with #{inner_exception.inspect}: #{inner_exception.backtrace.join("\n")}")
-
end
-
end
-
end
-
raise exception
-
end
-
end
-
-
# @api private
-
1
def state_for(adapter)
-
unless @adapters.include?(adapter)
-
raise "Unknown adapter #{adapter}"
-
end
-
-
@adapters[adapter]
-
end
-
-
# @api private
-
1
def do_adapter(adapter, what, prerequisite)
-
unless @transaction_primitives.include?(adapter)
-
raise "No primitive for #{adapter}"
-
end
-
-
state = state_for(adapter)
-
-
unless state == prerequisite
-
raise "Illegal state for #{what}: #{state}"
-
end
-
-
DataMapper.logger.debug("#{adapter.name}: #{what}")
-
@transaction_primitives[adapter].send(what)
-
@adapters[adapter] = what
-
end
-
-
# @api private
-
1
def log_fatal_transaction_breakage(adapter)
-
DataMapper.logger.fatal("#{self} experienced a totally broken transaction execution. Presenting member #{adapter.inspect}.")
-
end
-
-
# @api private
-
1
def connect_adapter(adapter)
-
if @transaction_primitives.key?(adapter)
-
raise "Already a primitive for adapter #{adapter}"
-
end
-
-
@transaction_primitives[adapter] = validate_primitive(adapter.transaction_primitive)
-
end
-
-
# @api private
-
1
def close_adapter_if_open(adapter)
-
if @transaction_primitives.include?(adapter)
-
close_adapter(adapter)
-
end
-
end
-
-
# @api private
-
1
def close_adapter(adapter)
-
unless @transaction_primitives.include?(adapter)
-
raise 'No primitive for adapter'
-
end
-
-
@transaction_primitives[adapter].close
-
@transaction_primitives.delete(adapter)
-
end
-
-
# @api private
-
1
def begin_adapter(adapter)
-
do_adapter(adapter, :begin, :none)
-
end
-
-
# @api private
-
1
def commit_adapter(adapter)
-
do_adapter(adapter, :commit, :begin)
-
end
-
-
# @api private
-
1
def rollback_adapter(adapter)
-
do_adapter(adapter, :rollback, :begin)
-
end
-
-
# @api private
-
1
def rollback_and_close_adapter(adapter)
-
rollback_adapter(adapter)
-
close_adapter(adapter)
-
end
-
-
1
module Repository
-
-
# Produce a new Transaction for this Repository
-
#
-
# @return [Adapters::Transaction]
-
# a new Transaction (in state :none) that can be used
-
# to execute code #with_transaction
-
#
-
# @api public
-
1
def transaction
-
Transaction.new(self)
-
end
-
end # module Repository
-
-
1
module Model
-
# @api private
-
1
def self.included(mod)
-
1
mod.descendants.each { |model| model.extend self }
-
end
-
-
# Produce a new Transaction for this Resource class
-
#
-
# @return <Adapters::Transaction
-
# a new Adapters::Transaction with all Repositories
-
# of the class of this Resource added.
-
#
-
# @api public
-
1
def transaction
-
transaction = Transaction.new(self)
-
transaction.commit { |block_args| yield(*block_args) }
-
end
-
end # module Model
-
-
1
module Resource
-
-
# Produce a new Transaction for the class of this Resource
-
#
-
# @return [Adapters::Transaction]
-
# a new Adapters::Transaction for the Repository
-
# of the class of this Resource added.
-
#
-
# @api public
-
1
def transaction
-
model.transaction { |*block_args| yield(*block_args) }
-
end
-
end # module Resource
-
-
1
def self.include_transaction_api
-
1
[ :Repository, :Model, :Resource ].each do |name|
-
3
DataMapper.const_get(name).send(:include, const_get(name))
-
end
-
1
Adapters::AbstractAdapter.descendants.each do |adapter_class|
-
Adapters.include_transaction_api(DataMapper::Inflector.demodulize(adapter_class.name))
-
end
-
end
-
-
end # class Transaction
-
-
1
module Adapters
-
-
1
def self.include_transaction_api(const_name)
-
2
require transaction_extensions(const_name)
-
2
if Transaction.const_defined?(const_name)
-
2
adapter = const_get(const_name)
-
2
adapter.send(:include, transaction_module(const_name))
-
end
-
rescue LoadError
-
# Silently ignore the fact that no adapter extensions could be required
-
# This means that the adapter in use doesn't support transactions
-
end
-
-
1
def self.transaction_module(const_name)
-
2
Transaction.const_get(const_name)
-
end
-
-
1
class << self
-
1
private
-
-
# @api private
-
1
def transaction_extensions(const_name)
-
2
name = adapter_name(const_name)
-
2
name = 'do' if name == 'dataobjects'
-
2
"dm-transactions/adapters/dm-#{name}-adapter"
-
end
-
-
end
-
-
1
extendable do
-
# @api private
-
1
def const_added(const_name)
-
2
include_transaction_api(const_name)
-
2
super
-
end
-
end
-
-
end # module Adapters
-
-
1
Transaction.include_transaction_api
-
-
end # module DataMapper
-
1
module DataMapper
-
1
class Transaction
-
-
1
module DataObjectsAdapter
-
1
extend Chainable
-
-
# Produces a fresh transaction primitive for this Adapter
-
#
-
# Used by Transaction to perform its various tasks.
-
#
-
# @return [Object]
-
# a new Object that responds to :close, :begin, :commit,
-
# and :rollback,
-
#
-
# @api private
-
1
def transaction_primitive
-
if current_transaction && supports_savepoints?
-
DataObjects::SavePoint.create_for_uri(normalized_uri, current_connection)
-
else
-
DataObjects::Transaction.create_for_uri(normalized_uri)
-
end
-
end
-
-
# Pushes the given Transaction onto the per thread Transaction stack so
-
# that everything done by this Adapter is done within the context of said
-
# Transaction.
-
#
-
# @param [Transaction] transaction
-
# a Transaction to be the 'current' transaction until popped.
-
#
-
# @return [Array(Transaction)]
-
# the stack of active transactions for the current thread
-
#
-
# @api private
-
1
def push_transaction(transaction)
-
transactions << transaction
-
end
-
-
# Pop the 'current' Transaction from the per thread Transaction stack so
-
# that everything done by this Adapter is no longer necessarily within the
-
# context of said Transaction.
-
#
-
# @return [Transaction]
-
# the former 'current' transaction.
-
#
-
# @api private
-
1
def pop_transaction
-
transactions.pop
-
end
-
-
# Retrieve the current transaction for this Adapter.
-
#
-
# Everything done by this Adapter is done within the context of this
-
# Transaction.
-
#
-
# @return [Transaction]
-
# the 'current' transaction for this Adapter.
-
#
-
# @api private
-
1
def current_transaction
-
452
transactions.last
-
end
-
-
1
chainable do
-
1
protected
-
-
# @api semipublic
-
1
def open_connection
-
226
current_connection || super
-
end
-
-
# @api semipublic
-
1
def close_connection(connection)
-
226
super unless current_connection.equal?(connection)
-
end
-
end
-
-
1
private
-
-
# @api private
-
1
def transactions
-
452
Thread.current[:dm_transactions] ||= {}
-
452
Thread.current[:dm_transactions][object_id] ||= []
-
end
-
-
# Retrieve the current connection for this Adapter.
-
#
-
# @return [Transaction]
-
# the 'current' connection for this Adapter.
-
#
-
# @api private
-
1
def current_connection
-
452
if transaction = current_transaction
-
transaction.primitive_for(self).connection
-
end
-
end
-
-
# Indicate whether adapter supports transactional savepoints. Not all DO
-
# adapters do, so default to false.
-
#
-
# @return [Boolean]
-
# whether or not the adapter supports savepoints
-
#
-
# @api private
-
1
def supports_savepoints?
-
false
-
end
-
-
end # module DataObjectsAdapter
-
-
end # class Transaction
-
end # module DataMapper
-
1
require 'dm-transactions/adapters/dm-do-adapter'
-
-
1
module DataMapper
-
1
class Transaction
-
-
1
module PostgresAdapter
-
1
include DataObjectsAdapter
-
-
1
def supports_savepoints?
-
true
-
end
-
end
-
-
end
-
end
-
1
require 'dm-core'
-
-
1
module DataMapper
-
1
class Property
-
1
autoload :CommaSeparatedList, 'dm-types/comma_separated_list'
-
1
autoload :Csv, 'dm-types/csv'
-
1
autoload :BCryptHash, 'dm-types/bcrypt_hash'
-
1
autoload :Enum, 'dm-types/enum'
-
1
autoload :EpochTime, 'dm-types/epoch_time'
-
1
autoload :FilePath, 'dm-types/file_path'
-
1
autoload :Flag, 'dm-types/flag'
-
1
autoload :IPAddress, 'dm-types/ip_address'
-
1
autoload :Json, 'dm-types/json'
-
1
autoload :Regexp, 'dm-types/regexp'
-
1
autoload :ParanoidBoolean, 'dm-types/paranoid_boolean'
-
1
autoload :ParanoidDateTime, 'dm-types/paranoid_datetime'
-
1
autoload :Slug, 'dm-types/slug'
-
1
autoload :UUID, 'dm-types/uuid'
-
1
autoload :URI, 'dm-types/uri'
-
1
autoload :Yaml, 'dm-types/yaml'
-
1
autoload :APIKey, 'dm-types/api_key'
-
end
-
end
-
1
require 'dm-core'
-
1
require 'dm-validations/support/ordered_hash'
-
1
require 'dm-validations/support/object'
-
-
1
require 'dm-validations/exceptions'
-
1
require 'dm-validations/validation_errors'
-
1
require 'dm-validations/contextual_validators'
-
1
require 'dm-validations/auto_validate'
-
1
require 'dm-validations/context'
-
-
1
require 'dm-validations/validators/generic_validator'
-
1
require 'dm-validations/validators/required_field_validator'
-
1
require 'dm-validations/validators/primitive_validator'
-
1
require 'dm-validations/validators/absent_field_validator'
-
1
require 'dm-validations/validators/confirmation_validator'
-
1
require 'dm-validations/validators/format_validator'
-
1
require 'dm-validations/validators/length_validator'
-
1
require 'dm-validations/validators/within_validator'
-
1
require 'dm-validations/validators/numeric_validator'
-
1
require 'dm-validations/validators/method_validator'
-
1
require 'dm-validations/validators/block_validator'
-
1
require 'dm-validations/validators/uniqueness_validator'
-
1
require 'dm-validations/validators/acceptance_validator'
-
-
1
module DataMapper
-
1
module Validations
-
-
1
Model.append_inclusions self
-
-
1
def self.included(model)
-
5
model.extend ClassMethods
-
end
-
-
# Ensures the object is valid for the context provided, and otherwise
-
# throws :halt and returns false.
-
#
-
# @api public
-
1
def save(context = default_validation_context)
-
51
model.validators.assert_valid(context)
-
102
Validations::Context.in_context(context) { super() }
-
end
-
-
# @api public
-
1
def update(attributes = {}, context = default_validation_context)
-
model.validators.assert_valid(context)
-
Validations::Context.in_context(context) { super(attributes) }
-
end
-
-
# @api private
-
1
def save_self(*)
-
52
if Validations::Context.any? && !valid?(model.validators.current_context)
-
24
false
-
else
-
28
super
-
end
-
end
-
-
# Return the ValidationErrors
-
#
-
# @api public
-
1
def errors
-
76
@errors ||= ValidationErrors.new(self)
-
end
-
-
# Mark this resource as validatable. When we validate associations of a
-
# resource we can check if they respond to validatable? before trying to
-
# recursively validate them
-
#
-
# @api semipublic
-
1
def validatable?
-
true
-
end
-
-
# Alias for valid?(:default)
-
#
-
# TODO: deprecate
-
1
def valid_for_default?
-
valid?(:default)
-
end
-
-
# Check if a resource is valid in a given context
-
#
-
# @api public
-
1
def valid?(context = :default)
-
52
model = respond_to?(:model) ? self.model : self.class
-
52
model.validators.execute(context, self)
-
end
-
-
# @api semipublic
-
1
def validation_property_value(name)
-
689
__send__(name) if respond_to?(name, true)
-
end
-
-
1
module ClassMethods
-
1
include DataMapper::Validations::ValidatesPresence
-
1
include DataMapper::Validations::ValidatesAbsence
-
1
include DataMapper::Validations::ValidatesConfirmation
-
1
include DataMapper::Validations::ValidatesPrimitiveType
-
1
include DataMapper::Validations::ValidatesAcceptance
-
1
include DataMapper::Validations::ValidatesFormat
-
1
include DataMapper::Validations::ValidatesLength
-
1
include DataMapper::Validations::ValidatesWithin
-
1
include DataMapper::Validations::ValidatesNumericality
-
1
include DataMapper::Validations::ValidatesWithMethod
-
1
include DataMapper::Validations::ValidatesWithBlock
-
1
include DataMapper::Validations::ValidatesUniqueness
-
1
include DataMapper::Validations::AutoValidations
-
-
# Return the set of contextual validators or create a new one
-
#
-
# @api public
-
1
def validators
-
252
@validators ||= ContextualValidators.new(self)
-
end
-
-
# @api private
-
1
def inherited(base)
-
super
-
self.validators.contexts.each do |context, validators|
-
validators.each do |v|
-
options = v.options.merge(:context => context)
-
base.validators.add(v.class, v.field_name, options)
-
end
-
end
-
end
-
-
# @api public
-
1
def create(attributes = {}, *args)
-
32
resource = new(attributes)
-
32
resource.save(*args)
-
32
resource
-
end
-
-
1
private
-
-
# Given a new context create an instance method of
-
# valid_for_<context>? which simply calls valid?(context)
-
# if it does not already exist
-
#
-
# @api private
-
1
def self.create_context_instance_methods(model, context)
-
# TODO: deprecate `valid_for_#{context}?`
-
# what's wrong with requiring the caller to pass the context as an arg?
-
# eg, `valid?(:context)`
-
# these methods are handy for symbol-based callbacks,
-
# eg. `:if => :valid_for_context?`
-
# but these methods are so trivial to add where needed, making it
-
# overkill to do this for all contexts on all validated objects.
-
46
context = context.to_sym
-
-
46
name = "valid_for_#{context}?"
-
46
present = model.respond_to?(:resource_method_defined) ? model.resource_method_defined?(name) : model.instance_methods.include?(name)
-
46
unless present
-
46
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name} # def valid_for_signup?
-
valid?(#{context.inspect}) # valid?(:signup)
-
end # end
-
RUBY
-
end
-
end
-
-
end # module ClassMethods
-
end # module Validations
-
-
# Provide a const alias for backward compatibility with plugins
-
# This is scheduled to go away though, definitely before 1.0
-
1
Validate = Validations
-
-
end # module DataMapper
-
1
module DataMapper
-
# for options_with_message
-
1
Property.accept_options :message, :messages, :set, :validates, :auto_validation, :format
-
-
1
module Validations
-
1
module AutoValidations
-
-
1
module ModelExtension
-
# @api private
-
1
def property(*)
-
20
property = super
-
20
AutoValidations.generate_for_property(property)
-
# FIXME: explicit return needed for YARD to parse this properly
-
20
return property
-
end
-
-
1
Model.append_extensions self
-
end # module ModelExtension
-
-
-
# TODO: why are there 3 entry points to this ivar?
-
# #disable_auto_validations, #disabled_auto_validations?, #auto_validations_disabled?
-
1
attr_reader :disable_auto_validations
-
-
# Checks whether auto validations are currently
-
# disabled (see +disable_auto_validations+ method
-
# that takes a block)
-
#
-
# @return [TrueClass, FalseClass]
-
# true if auto validation is currently disabled
-
#
-
# @api semipublic
-
1
def disabled_auto_validations?
-
20
@disable_auto_validations || false
-
end
-
-
# TODO: deprecate all but one of these 3 variants
-
1
alias_method :auto_validations_disabled?, :disabled_auto_validations?
-
-
# disables generation of validations for
-
# duration of given block
-
#
-
# @api public
-
1
def without_auto_validations
-
previous, @disable_auto_validations = @disable_auto_validations, true
-
yield
-
ensure
-
@disable_auto_validations = previous
-
end
-
-
# Auto-generate validations for a given property. This will only occur
-
# if the option :auto_validation is either true or left undefined.
-
#
-
# Triggers that generate validator creation
-
#
-
# :required => true
-
# Setting the option :required to true causes a
-
# validates_presence_of validator to be automatically created on
-
# the property
-
#
-
# :length => 20
-
# Setting the option :length causes a validates_length_of
-
# validator to be automatically created on the property. If the
-
# value is a Integer the validation will set :maximum => value
-
# if the value is a Range the validation will set
-
# :within => value
-
#
-
# :format => :predefined / lambda / Proc
-
# Setting the :format option causes a validates_format_of
-
# validator to be automatically created on the property
-
#
-
# :set => ["foo", "bar", "baz"]
-
# Setting the :set option causes a validates_within
-
# validator to be automatically created on the property
-
#
-
# Integer type
-
# Using a Integer type causes a validates_numericality_of
-
# validator to be created for the property. integer_only
-
# is set to true
-
#
-
# BigDecimal or Float type
-
# Using a Integer type causes a validates_numericality_of
-
# validator to be created for the property. integer_only
-
# is set to false, and precision/scale match the property
-
#
-
#
-
# Messages
-
#
-
# :messages => {..}
-
# Setting :messages hash replaces standard error messages
-
# with custom ones. For instance:
-
# :messages => {:presence => "Field is required",
-
# :format => "Field has invalid format"}
-
# Hash keys are: :presence, :format, :length, :is_unique,
-
# :is_number, :is_primitive
-
#
-
# :message => "Some message"
-
# It is just shortcut if only one validation option is set
-
#
-
# @api private
-
1
def self.generate_for_property(property)
-
return if (property.model.disabled_auto_validations? ||
-
20
skip_auto_validation_for?(property))
-
-
# all auto-validations (aside from presence) should skip
-
# validation when the value is nil
-
20
opts = { :allow_nil => true }
-
-
20
if property.options.key?(:validates)
-
opts[:context] = property.options[:validates]
-
end
-
-
20
infer_presence_validation_for(property, opts.dup)
-
20
infer_length_validation_for(property, opts.dup)
-
20
infer_format_validation_for(property, opts.dup)
-
20
infer_uniqueness_validation_for(property, opts.dup)
-
20
infer_within_validation_for(property, opts.dup)
-
20
infer_type_validation_for(property, opts.dup)
-
end # generate_for_property
-
-
1
private
-
-
# Checks whether or not property should be auto validated.
-
# It is the case for properties with :auto_validation option
-
# given and it's value evaluates to true
-
#
-
# @return [TrueClass, FalseClass]
-
# true for properties with :auto_validation option that has
-
# positive value
-
#
-
# @api private
-
1
def self.skip_auto_validation_for?(property)
-
(property.options.key?(:auto_validation) &&
-
20
!property.options[:auto_validation])
-
end
-
-
# @api private
-
1
def self.infer_presence_validation_for(property, options)
-
20
return if skip_presence_validation?(property)
-
-
14
validation_options = options_with_message(options, property, :presence)
-
14
property.model.validates_presence_of property.name, validation_options
-
end
-
-
# @api private
-
1
def self.skip_presence_validation?(property)
-
20
property.allow_blank? || property.serial?
-
end
-
-
# @api private
-
1
def self.infer_length_validation_for(property, options)
-
return unless (property.kind_of?(DataMapper::Property::String) ||
-
20
property.kind_of?(DataMapper::Property::Text))
-
-
7
length = property.options.fetch(:length, DataMapper::Property::String::DEFAULT_LENGTH)
-
-
-
7
if length.is_a?(Range)
-
raise ArgumentError, "Infinity is no valid upper bound for a length range" if length.last == Infinity
-
options[:within] = length
-
else
-
7
options[:maximum] = length
-
end
-
-
7
validation_options = options_with_message(options, property, :length)
-
7
property.model.validates_length_of property.name, validation_options
-
end
-
-
# @api private
-
1
def self.infer_format_validation_for(property, options)
-
20
return unless property.options.key?(:format)
-
-
1
options[:with] = property.options[:format]
-
-
1
validation_options = options_with_message(options, property, :format)
-
1
property.model.validates_format_of property.name, validation_options
-
end
-
-
# @api private
-
1
def self.infer_uniqueness_validation_for(property, options)
-
20
return unless property.options.key?(:unique)
-
-
9
case value = property.options[:unique]
-
when Array, Symbol
-
options[:scope] = Array(value)
-
-
validation_options = options_with_message(options, property, :is_unique)
-
property.model.validates_uniqueness_of property.name, validation_options
-
when TrueClass
-
3
validation_options = options_with_message(options, property, :is_unique)
-
3
property.model.validates_uniqueness_of property.name, validation_options
-
end
-
end
-
-
# @api private
-
1
def self.infer_within_validation_for(property, options)
-
20
return unless property.options.key?(:set)
-
-
options[:set] = property.options[:set]
-
-
validation_options = options_with_message(options, property, :within)
-
property.model.validates_within property.name, validation_options
-
end
-
-
# @api private
-
1
def self.infer_type_validation_for(property, options)
-
20
return if property.respond_to?(:custom?) && property.custom?
-
-
20
if property.kind_of?(Property::Numeric)
-
11
options[:gte] = property.min if property.min
-
11
options[:lte] = property.max if property.max
-
end
-
-
20
if Integer == property.primitive
-
11
options[:integer_only] = true
-
-
11
validation_options = options_with_message(options, property, :is_number)
-
11
property.model.validates_numericality_of property.name, validation_options
-
elsif (BigDecimal == property.primitive ||
-
9
Float == property.primitive)
-
options[:precision] = property.precision
-
options[:scale] = property.scale
-
-
validation_options = options_with_message(options, property, :is_number)
-
property.model.validates_numericality_of property.name, validation_options
-
else
-
# We only need this in the case we don't already
-
# have a numeric validator, because otherwise
-
# it will cause duplicate validation errors
-
9
validation_options = options_with_message(options, property, :is_primitive)
-
9
property.model.validates_primitive_type_of property.name, validation_options
-
end
-
end
-
-
# adds message for validator
-
#
-
# @api private
-
1
def self.options_with_message(base_options, property, validator_name)
-
45
options = base_options.clone
-
45
opts = property.options
-
-
45
if opts.key?(:messages)
-
options[:message] = opts[:messages][validator_name]
-
45
elsif opts.key?(:message)
-
options[:message] = opts[:message]
-
end
-
-
45
options
-
end
-
end # module AutoValidations
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# Module with validation context functionality.
-
#
-
# Contexts are implemented using a thread-local array-based stack.
-
#
-
1
module Context
-
# Execute a block of code within a specific validation context
-
#
-
# @param [Symbol] context
-
# the context to execute the block of code within
-
#
-
# @api semipublic
-
1
def self.in_context(context)
-
51
stack << context
-
51
yield
-
ensure
-
51
stack.pop
-
end
-
-
# Get the current validation context or nil (if no context is on the stack).
-
#
-
# @return [Symbol, NilClass]
-
# The current validation context (for the current thread),
-
# or nil if no current context is on the stack
-
1
def self.current
-
103
stack.last
-
end
-
-
# Are there any contexts on the stack?
-
#
-
# @return [Boolean]
-
# true/false whether there are any contexts on the context stack
-
#
-
# @api semipublic
-
1
def self.any?(&block)
-
52
stack.any?(&block)
-
end
-
-
# The (thread-local) validation context stack
-
# This allows object graphs to be saved within potentially nested contexts
-
# without having to pass the validation context throughout
-
#
-
# @api private
-
1
def self.stack
-
257
Thread.current[:dm_validations_context_stack] ||= []
-
end
-
-
# The default validation context for this Resource.
-
# This Resource's default context can be overridden by implementing
-
# #default_validation_context
-
#
-
# @return [Symbol]
-
# the current validation context from the context stack
-
# (if valid for this model), or :default
-
#
-
# @api semipublic
-
1
def default_validation_context
-
51
model.validators.current_context || :default
-
end
-
-
end # module Context
-
-
1
include Context
-
end # module Validations
-
end # module DataMapper
-
1
require 'forwardable'
-
-
1
module DataMapper
-
1
module Validations
-
#
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class ContextualValidators
-
1
extend Forwardable
-
-
#
-
# Delegators
-
#
-
-
1
def_delegators :@contexts, :empty?, :each
-
1
def_delegators :@attributes, :[]
-
1
include Enumerable
-
-
1
attr_reader :contexts, :attributes
-
-
1
def initialize(model = nil)
-
5
@model = model
-
5
@contexts = {}
-
5
@attributes = {}
-
end
-
-
#
-
# API
-
#
-
-
# Return an array of validators for a named context
-
#
-
# @param [String]
-
# Context name for which to return validators
-
# @return [Array<DataMapper::Validations::GenericValidator>]
-
# An array of validators bound to the given context
-
1
def context(name)
-
98
contexts[name] ||= OrderedSet.new
-
end
-
-
# Return an array of validators for a named property
-
#
-
# @param [Symbol]
-
# Property name for which to return validators
-
# @return [Array<DataMapper::Validations::GenericValidator>]
-
# An array of validators bound to the given property
-
1
def attribute(name)
-
46
attributes[name] ||= OrderedSet.new
-
end
-
-
# Clear all named context validators off of the resource
-
#
-
1
def clear!
-
contexts.clear
-
attributes.clear
-
end
-
-
# Create a new validator of the given klazz and push it onto the
-
# requested context for each of the attributes in +attributes+
-
#
-
# @param [DataMapper::Validations::GenericValidator] validator_class
-
# Validator class, example: DataMapper::Validations::LengthValidator
-
#
-
# @param [Array<Symbol>] attributes
-
# Attribute names given to validation macro, example:
-
# [:first_name, :last_name] in validates_presence_of :first_name, :last_name
-
#
-
# @param [Hash] options
-
# Options supplied to validation macro, example:
-
# {:context=>:default, :maximum=>50, :allow_nil=>true, :message=>nil}
-
#
-
# @option [Symbol] :context
-
# the context in which the new validator should be run
-
# @option [Boolean] :allow_nil
-
# whether or not the new validator should allow nil values
-
# @option [Boolean] :message
-
# the error message the new validator will provide on validation failure
-
1
def add(validator_class, *attributes)
-
46
options = attributes.last.kind_of?(Hash) ? attributes.pop.dup : {}
-
46
normalize_options(options)
-
46
validator_options = DataMapper::Ext::Hash.except(options, :context)
-
-
46
attributes.each do |attribute|
-
# TODO: is :context part of the Validator state (ie, intrinsic),
-
# or is it just membership in a collection?
-
46
validator = validator_class.new(attribute, validator_options)
-
46
attribute_validators = self.attribute(attribute)
-
46
attribute_validators << validator unless attribute_validators.include?(validator)
-
-
46
options[:context].each do |context|
-
46
context_validators = self.context(context)
-
46
next if context_validators.include?(validator)
-
46
context_validators << validator
-
# TODO: eliminate this, then eliminate the @model ivar entirely
-
46
Validations::ClassMethods.create_context_instance_methods(@model, context) if @model
-
end
-
end
-
end
-
-
# Clean up the argument list and return a opts hash, including the
-
# merging of any default opts. Set the context to default if none is
-
# provided. Also allow :context to be aliased to :on, :when & :group
-
#
-
# @param [Hash] options
-
# the options to be normalized
-
# @param [NilClass, Hash] defaults
-
# default keys/values to set on normalized options
-
#
-
# @return [Hash]
-
# the normalized options
-
#
-
# @api private
-
1
def normalize_options(options, defaults = nil)
-
46
context = [
-
options.delete(:group),
-
options.delete(:on),
-
options.delete(:when),
-
options.delete(:context)
-
].compact.first
-
-
46
options[:context] = Array(context || :default)
-
46
options.update(defaults) unless defaults.nil?
-
46
options
-
end
-
-
# Returns the current validation context on the stack if valid for this model,
-
# nil if no contexts are defined for the model (and no contexts are on
-
# the validation stack), or :default if the current context is invalid for
-
# this model or no contexts have been defined for this model and
-
# no context is on the stack.
-
#
-
# @return [Symbol]
-
# the current validation context from the stack (if valid for this model),
-
# nil if no context is on the stack and no contexts are defined for this model,
-
# or :default if the context on the stack is invalid for this model or
-
# no context is on the stack and this model has at least one validation context
-
#
-
# @api private
-
#
-
# TODO: simplify the semantics of #current_context, #valid?
-
1
def current_context
-
103
context = Validations::Context.current
-
103
valid_context?(context) ? context : :default
-
end
-
-
# Test if the context is valid for the model
-
#
-
# @param [Symbol] context
-
# the context to test
-
#
-
# @return [Boolean]
-
# true if the context is valid for the model
-
#
-
# @api private
-
#
-
# TODO: investigate removing the `contexts.empty?` test here.
-
1
def valid_context?(context)
-
154
contexts.empty? || contexts.include?(context)
-
end
-
-
# Assert that the given context is valid for this model
-
#
-
# @param [Symbol] context
-
# the context to test
-
#
-
# @raise [InvalidContextError]
-
# raised if the context is not valid for this model
-
#
-
# @api private
-
#
-
# TODO: is this method actually needed?
-
1
def assert_valid(context)
-
51
unless valid_context?(context)
-
raise InvalidContextError, "#{context} is an invalid context, known contexts are #{contexts.keys.inspect}"
-
end
-
end
-
-
# Execute all validators in the named context against the target.
-
# Load together any properties that are designated lazy but are not
-
# yet loaded.
-
#
-
# @param [Symbol] named_context
-
# the context we are validating against
-
# @param [Object] target
-
# the resource that we are validating
-
# @return [Boolean]
-
# true if all are valid, otherwise false
-
1
def execute(named_context, target)
-
52
target.errors.clear!
-
-
52
available_validators = context(named_context)
-
741
executable_validators = available_validators.select { |v| v.execute?(target) }
-
-
# In the case of a new Resource or regular ruby class instance,
-
# everything needs to be validated completely, and no eager-loading
-
# logic should apply.
-
#
-
52
if target.kind_of?(DataMapper::Resource) && !target.new?
-
8
load_validated_properties(target, executable_validators)
-
end
-
741
executable_validators.map { |validator| validator.call(target) }.all?
-
end
-
-
# Load all lazy, not-yet-loaded properties that need validation,
-
# all at once.
-
1
def load_validated_properties(resource, validators)
-
8
properties = resource.model.properties
-
-
8
properties_to_load = validators.map { |validator|
-
136
properties[validator.field_name]
-
}.compact.select { |property|
-
128
property.lazy? && !property.loaded?(resource)
-
}
-
-
8
resource.__send__(:eager_load, properties_to_load)
-
end
-
-
end # module ContextualValidators
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
class ValidationError < StandardError; end
-
-
1
class InvalidContextError < StandardError; end
-
end
-
# encoding: UTF-8
-
-
1
module DataMapper
-
1
module Validations
-
1
module Format
-
1
module Email
-
-
1
def self.included(base)
-
1
DataMapper::Validations::FormatValidator::FORMATS.merge!(
-
:email_address => [
-
EmailAddress,
-
lambda { |field, value|
-
'%s is not a valid email address'.t(value)
-
}
-
]
-
)
-
end
-
-
# Almost RFC2822 (No attribution reference available).
-
#
-
# This differs in that it does not allow local domains (test@localhost).
-
# 99% of the time you do not want to allow these email addresses
-
# in a public web application.
-
1
EmailAddress = begin
-
1
if (RUBY_VERSION == '1.9.2' && RUBY_ENGINE == 'jruby' && JRUBY_VERSION <= '1.6.3') || RUBY_VERSION == '1.9.3'
-
# There is an obscure bug in jruby 1.6 that prevents matching
-
# on unicode properties here. Remove this logic branch once
-
# a stable jruby release fixes this.
-
#
-
# http://jira.codehaus.org/browse/JRUBY-5622
-
#
-
# There is a similar bug in preview releases of 1.9.3
-
#
-
# http://redmine.ruby-lang.org/issues/5126
-
letter = 'a-zA-Z'
-
else
-
1
letter = 'a-zA-Z\p{L}' # Changed from RFC2822 to include unicode chars
-
end
-
1
digit = '0-9'
-
1
atext = "[#{letter}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]"
-
1
dot_atom_text = "#{atext}+([.]#{atext}*)+"
-
1
dot_atom = dot_atom_text
-
1
no_ws_ctl = '\x01-\x08\x11\x12\x14-\x1f\x7f'
-
1
qtext = "[^#{no_ws_ctl}\\x0d\\x22\\x5c]" # Non-whitespace, non-control character except for \ and "
-
1
text = '[\x01-\x09\x11\x12\x14-\x7f]'
-
1
quoted_pair = "(\\x5c#{text})"
-
1
qcontent = "(?:#{qtext}|#{quoted_pair})"
-
1
quoted_string = "[\"]#{qcontent}+[\"]"
-
1
atom = "#{atext}+"
-
1
word = "(?:#{atom}|#{quoted_string})"
-
1
obs_local_part = "#{word}([.]#{word})*"
-
1
local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})"
-
1
dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]"
-
1
dcontent = "(?:#{dtext}|#{quoted_pair})"
-
1
domain_literal = "\\[#{dcontent}+\\]"
-
1
obs_domain = "#{atom}([.]#{atom})+"
-
1
domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})"
-
1
addr_spec = "#{local_part}\@#{domain}"
-
1
pattern = /\A#{addr_spec}\z/u
-
end
-
-
end # module Email
-
end # module Format
-
end # module Validations
-
end # module DataMapper
-
# encoding: utf-8
-
-
1
module DataMapper
-
1
module Validations
-
1
module Format
-
1
module Url
-
-
1
def self.included(base)
-
1
DataMapper::Validations::FormatValidator::FORMATS.merge!(
-
:url => [
-
Url,
-
lambda { |field, value|
-
'%s is not a valid URL'.t(value)
-
}
-
]
-
)
-
end
-
-
1
Url = begin
-
# Regex from http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/
-
1
/(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}((\:[0-9]{1,5})?\/?.*)?$)/ix
-
end
-
-
end # module Url
-
end # module Format
-
end # module Validations
-
end # module DataMapper
-
1
class Object
-
# If receiver is callable, calls it and returns result.
-
# If not, just returns receiver itself
-
#
-
# @return [Object]
-
# @api private
-
1
def try_call(*args)
-
24
if self.respond_to?(:call)
-
self.call(*args)
-
else
-
24
self
-
end
-
end
-
-
1
def validatable?
-
false
-
end
-
end
-
2
module DataMapper; module Validations
-
# TITLE:
-
#
-
# OrderedHash (originally Dictionary)
-
#
-
# AUTHORS:
-
#
-
# - Jan Molic
-
# - Thomas Sawyer
-
#
-
# CREDIT:
-
#
-
# - Andrew Johnson (merge, to_a, inspect, shift and Hash[])
-
# - Jeff Sharpe (reverse and reverse!)
-
# - Thomas Leitner (has_key? and key?)
-
#
-
# LICENSE:
-
#
-
# Copyright (c) 2005 Jan Molic, Thomas Sawyer
-
#
-
# Ruby License
-
#
-
# This module is free software. You may use, modify, and/or redistribute this
-
# software under the same terms as Ruby.
-
#
-
# This program is distributed in the hope that it will be useful, but WITHOUT
-
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-
# FOR A PARTICULAR PURPOSE.
-
#
-
# Originally ported from OrderHash 2.0, Copyright (c) 2005 jan molic
-
#
-
# LOG:
-
#
-
# - 2007.10.31 trans
-
# Fixed initialize so the constructor blocks correctly effected dictionary
-
# rather then just the internal hash.
-
-
# = Dictionary
-
#
-
# The Dictionary class is a Hash that preserves order.
-
# So it has some array-like extensions also. By defualt
-
# a Dictionary object preserves insertion order, but any
-
# order can be specified including alphabetical key order.
-
#
-
# == Usage
-
#
-
# Just require this file and use Dictionary instead of Hash.
-
#
-
# # You can do simply
-
# hsh = Dictionary.new
-
# hsh['z'] = 1
-
# hsh['a'] = 2
-
# hsh['c'] = 3
-
# p hsh.keys #=> ['z','a','c']
-
#
-
# # or using Dictionary[] method
-
# hsh = Dictionary['z', 1, 'a', 2, 'c', 3]
-
# p hsh.keys #=> ['z','a','c']
-
#
-
# # but this doesn't preserve order
-
# hsh = Dictionary['z'=>1, 'a'=>2, 'c'=>3]
-
# p hsh.keys #=> ['a','c','z']
-
#
-
# # Dictionary has useful extensions: push, pop and unshift
-
# p hsh.push('to_end', 15) #=> true, key added
-
# p hsh.push('to_end', 30) #=> false, already - nothing happen
-
# p hsh.unshift('to_begin', 50) #=> true, key added
-
# p hsh.unshift('to_begin', 60) #=> false, already - nothing happen
-
# p hsh.keys #=> ["to_begin", "a", "c", "z", "to_end"]
-
# p hsh.pop #=> ["to_end", 15], if nothing remains, return nil
-
# p hsh.keys #=> ["to_begin", "a", "c", "z"]
-
# p hsh.shift #=> ["to_begin", 30], if nothing remains, return nil
-
#
-
# == Usage Notes
-
#
-
# * You can use #order_by to set internal sort order.
-
# * #<< takes a two element [k,v] array and inserts.
-
# * Use ::auto which creates Dictionay sub-entries as needed.
-
# * And ::alpha which creates a new Dictionary sorted by key.
-
1
class OrderedHash
-
-
1
include Enumerable
-
-
1
class << self
-
#--
-
# TODO is this needed? Doesn't the super class do this?
-
#++
-
1
def [](*args)
-
hsh = new
-
if Hash === args[0]
-
hsh.replace(args[0])
-
elsif (args.size % 2) != 0
-
raise ArgumentError, "odd number of elements for Hash"
-
else
-
while !args.empty?
-
hsh[args.shift] = args.shift
-
end
-
end
-
hsh
-
end
-
-
# Like #new but the block sets the order.
-
#
-
1
def new_by(*args, &blk)
-
new(*args).order_by(&blk)
-
end
-
-
# Alternate to #new which creates a dictionary sorted by key.
-
#
-
# d = Dictionary.alpha
-
# d["z"] = 1
-
# d["y"] = 2
-
# d["x"] = 3
-
# d #=> {"x"=>3,"y"=>2,"z"=>2}
-
#
-
# This is equivalent to:
-
#
-
# Dictionary.new.order_by { |key,value| key }
-
1
def alpha(*args, &block)
-
new(*args, &block).order_by_key
-
end
-
-
# Alternate to #new which auto-creates sub-dictionaries as needed.
-
#
-
# d = Dictionary.auto
-
# d["a"]["b"]["c"] = "abc" #=> { "a"=>{"b"=>{"c"=>"abc"}}}
-
#
-
1
def auto(*args)
-
#AutoDictionary.new(*args)
-
leet = lambda { |hsh, key| hsh[key] = new(&leet) }
-
new(*args, &leet)
-
end
-
end
-
-
# New Dictiionary.
-
1
def initialize(*args, &blk)
-
33
@order = []
-
33
@order_by = nil
-
33
if blk
-
33
dict = self # This ensure autmatic key entry effect the
-
57
oblk = lambda{ |hsh, key| blk[dict,key] } # dictionary rather then just the interal hash.
-
33
@hash = Hash.new(*args, &oblk)
-
else
-
@hash = Hash.new(*args)
-
end
-
end
-
-
1
def order
-
reorder if @order_by
-
@order
-
end
-
-
# Keep dictionary sorted by a specific sort order.
-
1
def order_by( &block )
-
@order_by = block
-
order
-
self
-
end
-
-
# Keep dictionary sorted by key.
-
#
-
# d = Dictionary.new.order_by_key
-
# d["z"] = 1
-
# d["y"] = 2
-
# d["x"] = 3
-
# d #=> {"x"=>3,"y"=>2,"z"=>2}
-
#
-
# This is equivalent to:
-
#
-
# Dictionary.new.order_by { |key,value| key }
-
#
-
# The initializer Dictionary#alpha also provides this.
-
1
def order_by_key
-
@order_by = lambda { |k,v| k }
-
order
-
self
-
end
-
-
# Keep dictionary sorted by value.
-
#
-
# d = Dictionary.new.order_by_value
-
# d["z"] = 1
-
# d["y"] = 2
-
# d["x"] = 3
-
# d #=> {"x"=>3,"y"=>2,"z"=>2}
-
#
-
# This is equivalent to:
-
#
-
# Dictionary.new.order_by { |key,value| value }
-
1
def order_by_value
-
@order_by = lambda { |k,v| v }
-
order
-
self
-
end
-
-
#
-
1
def reorder
-
if @order_by
-
assoc = @order.collect{ |k| [k,@hash[k]] }.sort_by(&@order_by)
-
@order = assoc.collect{ |k,v| k }
-
end
-
@order
-
end
-
-
1
def ==(hsh2)
-
if hsh2.is_a?( Dictionary )
-
@order == hsh2.order &&
-
@hash == hsh2.instance_variable_get("@hash")
-
else
-
false
-
end
-
end
-
-
1
def [] k
-
24
@hash[ k ]
-
end
-
-
1
def fetch(k, *a, &b)
-
@hash.fetch(k, *a, &b)
-
end
-
-
# Store operator.
-
#
-
# h[key] = value
-
#
-
# Or with additional index.
-
#
-
# h[key,index] = value
-
1
def []=(k, i=nil, v=nil)
-
24
if v
-
insert(i,k,v)
-
else
-
24
store(k,i)
-
end
-
end
-
-
1
def insert( i,k,v )
-
@order.insert( i,k )
-
@hash.store( k,v )
-
end
-
-
1
def store( a,b )
-
24
@order.push( a ) unless @hash.has_key?( a )
-
24
@hash.store( a,b )
-
end
-
-
1
def clear
-
52
@order = []
-
52
@hash.clear
-
end
-
-
1
def delete( key )
-
@order.delete( key )
-
@hash.delete( key )
-
end
-
-
1
def each_key
-
order.each { |k| yield( k ) }
-
self
-
end
-
-
1
def each_value
-
order.each { |k| yield( @hash[k] ) }
-
self
-
end
-
-
1
def each
-
order.each { |k| yield( k,@hash[k] ) }
-
self
-
end
-
1
alias each_pair each
-
-
1
def delete_if
-
order.clone.each { |k| delete k if yield(k,@hash[k]) }
-
self
-
end
-
-
1
def values
-
ary = []
-
order.each { |k| ary.push @hash[k] }
-
ary
-
end
-
-
1
def keys
-
order
-
end
-
-
1
def invert
-
hsh2 = self.class.new
-
order.each { |k| hsh2[@hash[k]] = k }
-
hsh2
-
end
-
-
1
def reject( &block )
-
self.dup.delete_if(&block)
-
end
-
-
1
def reject!( &block )
-
hsh2 = reject(&block)
-
self == hsh2 ? nil : hsh2
-
end
-
-
1
def replace( hsh2 )
-
@order = hsh2.order
-
@hash = hsh2.hash
-
end
-
-
1
def shift
-
key = order.first
-
key ? [key,delete(key)] : super
-
end
-
-
1
def unshift( k,v )
-
unless @hash.include?( k )
-
@order.unshift( k )
-
@hash.store( k,v )
-
true
-
else
-
false
-
end
-
end
-
-
1
def <<(kv)
-
push( *kv )
-
end
-
-
1
def push( k,v )
-
unless @hash.include?( k )
-
@order.push( k )
-
@hash.store( k,v )
-
true
-
else
-
false
-
end
-
end
-
-
1
def pop
-
key = order.last
-
key ? [key,delete(key)] : nil
-
end
-
-
1
def inspect
-
ary = []
-
each {|k,v| ary << k.inspect + "=>" + v.inspect}
-
'{' + ary.join(", ") + '}'
-
end
-
-
1
def dup
-
a = []
-
each{ |k,v| a << k; a << v }
-
self.class[*a]
-
end
-
-
1
def update( hsh2 )
-
hsh2.each { |k,v| self[k] = v }
-
reorder
-
self
-
end
-
1
alias :merge! update
-
-
1
def merge( hsh2 )
-
self.dup.update(hsh2)
-
end
-
-
1
def select
-
ary = []
-
each { |k,v| ary << [k,v] if yield k,v }
-
ary
-
end
-
-
1
def reverse!
-
@order.reverse!
-
self
-
end
-
-
1
def reverse
-
dup.reverse!
-
end
-
-
1
def first
-
@hash[order.first]
-
end
-
-
1
def last
-
@hash[order.last]
-
end
-
-
1
def length
-
@order.length
-
end
-
1
alias :size :length
-
-
1
def empty?
-
@hash.empty?
-
end
-
-
1
def has_key?(key)
-
@hash.has_key?(key)
-
end
-
-
1
def key?(key)
-
@hash.key?(key)
-
end
-
-
1
def to_a
-
ary = []
-
each { |k,v| ary << [k,v] }
-
ary
-
end
-
-
1
def to_json
-
buf = "["
-
map do |k,v|
-
buf << k.to_json
-
buf << ", "
-
buf << v.to_json
-
end.join(", ")
-
buf << "]"
-
buf
-
end
-
-
1
def to_s
-
self.to_a.to_s
-
end
-
-
1
def to_hash
-
@hash.dup
-
end
-
-
1
def to_h
-
@hash.dup
-
end
-
end
-
end; end
-
1
module DataMapper
-
1
module Validations
-
#
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class ValidationErrors
-
-
1
include Enumerable
-
-
1
@@default_error_messages = {
-
:absent => '%s must be absent',
-
:inclusion => '%s must be one of %s',
-
:invalid => '%s has an invalid format',
-
:confirmation => '%s does not match the confirmation',
-
:accepted => '%s is not accepted',
-
:nil => '%s must not be nil',
-
:blank => '%s must not be blank',
-
:length_between => '%s must be between %s and %s characters long',
-
:too_long => '%s must be at most %s characters long',
-
:too_short => '%s must be at least %s characters long',
-
:wrong_length => '%s must be %s characters long',
-
:taken => '%s is already taken',
-
:not_a_number => '%s must be a number',
-
:not_an_integer => '%s must be an integer',
-
:greater_than => '%s must be greater than %s',
-
:greater_than_or_equal_to => '%s must be greater than or equal to %s',
-
:equal_to => '%s must be equal to %s',
-
:not_equal_to => '%s must not be equal to %s',
-
:less_than => '%s must be less than %s',
-
:less_than_or_equal_to => '%s must be less than or equal to %s',
-
:value_between => '%s must be between %s and %s',
-
:primitive => '%s must be of type %s'
-
}
-
-
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
-
1
def self.default_error_messages=(default_error_messages)
-
@@default_error_messages = default_error_messages
-
end
-
-
1
def self.default_error_message(key, field, *values)
-
24
field = DataMapper::Inflector.humanize(field)
-
24
@@default_error_messages[key] % [field, *values].flatten
-
end
-
-
1
attr_reader :resource
-
-
1
def initialize(resource)
-
33
@resource = resource
-
57
@errors = DataMapper::Validations::OrderedHash.new { |h,k| h[k] = [] }
-
end
-
-
# Clear existing validation errors.
-
1
def clear!
-
52
errors.clear
-
end
-
-
# Add a validation error. Use the field_name :general if the errors
-
# does not apply to a specific field of the Resource.
-
#
-
# @param [Symbol] field_name
-
# The name of the field that caused the error
-
#
-
# @param [String] message
-
# The message to add
-
1
def add(field_name, message)
-
# see 6abe8fff in extlib, but don't enforce
-
# it unless Edge version is installed
-
24
if message.respond_to?(:try_call)
-
# DM resource
-
24
message = if (resource.respond_to?(:model) &&
-
24
resource.model.respond_to?(:properties))
-
24
message.try_call(
-
resource,
-
resource.model.properties[field_name]
-
)
-
else
-
# pure Ruby object
-
message.try_call(resource)
-
end
-
end
-
-
24
(errors[field_name] ||= []) << message
-
end
-
-
# Collect all errors into a single list.
-
1
def full_messages
-
errors.inject([]) do |list, pair|
-
list += pair.last
-
end
-
end
-
-
# Return validation errors for a particular field_name.
-
#
-
# @param [Symbol] field_name
-
# The name of the field you want an error for.
-
#
-
# @return [Array<DataMapper::Validations::Error>]
-
# Array of validation errors or empty array, if there are no errors
-
# on given field
-
1
def on(field_name)
-
errors_for_field = errors[field_name]
-
DataMapper::Ext.blank?(errors_for_field) ? nil : errors_for_field.uniq
-
end
-
-
1
def each
-
errors.each_value do |v|
-
yield(v) unless DataMapper::Ext.blank?(v)
-
end
-
end
-
-
1
def empty?
-
@errors.all? { |property_name, errors| errors.empty? }
-
end
-
-
1
def method_missing(meth, *args, &block)
-
errors.send(meth, *args, &block)
-
end
-
-
1
def respond_to?(method)
-
super || errors.respond_to?(method)
-
end
-
-
1
def [](property_name)
-
if (property_errors = errors[property_name.to_sym])
-
property_errors
-
end
-
end
-
-
1
private
-
-
1
def errors
-
76
@errors
-
end
-
-
end # class ValidationErrors
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
#
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class AbsenceValidator < GenericValidator
-
-
1
def call(target)
-
value = target.validation_property_value(field_name)
-
return true if DataMapper::Ext.blank?(value)
-
-
error_message = (
-
self.options[:message] || ValidationErrors.default_error_message(
-
:absent, field_name
-
)
-
)
-
-
add_error(target, error_message, field_name)
-
false
-
end
-
-
end # class AbsenceValidator
-
-
1
module ValidatesAbsence
-
1
extend Deprecate
-
-
# Validates that the specified attribute is "blank" via the
-
# attribute's #blank? method.
-
#
-
# @note
-
# dm-core's support lib adds the #blank? method to many classes,
-
# @see lib/dm-core/support/blank.rb (dm-core) for more information.
-
#
-
# @example [Usage]
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :unwanted_attribute, String
-
# property :another_unwanted, String
-
# property :yet_again, String
-
#
-
# validates_absence_of :unwanted_attribute
-
# validates_absence_of :another_unwanted, :yet_again
-
#
-
# # a call to valid? will return false unless
-
# # all three attributes are blank
-
# end
-
#
-
1
def validates_absence_of(*fields)
-
validators.add(AbsenceValidator, *fields)
-
end
-
-
1
deprecate :validates_absent, :validates_absence_of
-
end # module ValidatesAbsent
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Martin Kihlgren
-
# @since 0.9
-
1
class AcceptanceValidator < GenericValidator
-
-
1
def initialize(field_name, options = {})
-
super
-
-
@options[:allow_nil] = true unless @options.key?(:allow_nil)
-
-
@options[:accept] ||= [ '1', 1, 'true', true, 't' ]
-
@options[:accept] = Array(@options[:accept])
-
end
-
-
1
def call(target)
-
return true if valid?(target)
-
-
error_message = (
-
@options[:message] || ValidationErrors.default_error_message(
-
:accepted, field_name
-
)
-
)
-
add_error(target, error_message, field_name)
-
-
false
-
end
-
-
1
private
-
-
1
def valid?(target)
-
value = target.validation_property_value(field_name)
-
return true if allow_nil?(value)
-
@options[:accept].include?(value)
-
end
-
-
1
def allow_nil?(value)
-
@options[:allow_nil] && value.nil?
-
end
-
-
end # class AcceptanceValidator
-
-
1
module ValidatesAcceptance
-
1
extend Deprecate
-
-
# Validates that the attributes's value is in the set of accepted
-
# values.
-
#
-
# @option [Boolean] :allow_nil (true)
-
# true if nil is allowed, false if not allowed.
-
#
-
# @option [Array] :accept (["1", 1, "true", true, "t"])
-
# A list of accepted values.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :license_agreement_accepted, String
-
# property :terms_accepted, String
-
# validates_acceptance_of :license_agreement, :accept => "1"
-
# validates_acceptance_of :terms_accepted, :allow_nil => false
-
#
-
# # a call to valid? will return false unless:
-
# # license_agreement is nil or "1"
-
# # and
-
# # terms_accepted is one of ["1", 1, "true", true, "t"]
-
#
-
1
def validates_acceptance_of(*fields)
-
validators.add(AcceptanceValidator, *fields)
-
end
-
-
1
deprecate :validates_is_accepted, :validates_acceptance_of
-
-
end # module ValidatesIsAccepted
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author teamon
-
# @since 0.9
-
1
module ValidatesWithBlock
-
# Validate using the given block. The block given needs to return:
-
# [result::<Boolean>, Error Message::<String>]
-
#
-
# @example [Usage]
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :zip_code, String
-
#
-
# validates_with_block do
-
# if @zip_code == "94301"
-
# true
-
# else
-
# [false, "You're in the wrong zip code"]
-
# end
-
# end
-
#
-
# # A call to valid? will return false and
-
# # populate the object's errors with "You're in the
-
# # wrong zip code" unless zip_code == "94301"
-
#
-
# # You can also specify field:
-
#
-
# validates_with_block :zip_code do
-
# if @zip_code == "94301"
-
# true
-
# else
-
# [false, "You're in the wrong zip code"]
-
# end
-
# end
-
#
-
# # it will add returned error message to :zip_code field
-
#
-
1
def validates_with_block(*fields, &block)
-
@__validates_with_block_count ||= 0
-
@__validates_with_block_count += 1
-
-
# create method and pass it to MethodValidator
-
unless block_given?
-
raise ArgumentError, 'You need to pass a block to validates_with_block method'
-
end
-
-
method_name = "__validates_with_block_#{@__validates_with_block_count}".to_sym
-
define_method(method_name, &block)
-
-
options = fields.last.is_a?(Hash) ? fields.last.pop.dup : {}
-
options[:method] = method_name
-
fields = [method_name] if fields.empty?
-
-
validators.add(MethodValidator, *fields + [options])
-
end
-
end # module ValidatesWithMethod
-
end # module Validations
-
end # module DataMapper
-
# -*- coding: utf-8 -*-
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class ConfirmationValidator < GenericValidator
-
-
1
def initialize(field_name, options = {})
-
1
super
-
-
1
set_optional_by_default
-
-
1
@confirm_field_name = (
-
1
options[:confirm] || "#{field_name}_confirmation"
-
).to_sym
-
end
-
-
1
def call(target)
-
31
return true if valid?(target)
-
-
2
error_message = (
-
@options[:message] || ValidationErrors.default_error_message(
-
:confirmation, field_name
-
2
)
-
)
-
2
add_error(target, error_message, field_name)
-
-
2
false
-
end
-
-
1
private
-
-
1
def valid?(target)
-
31
value = target.validation_property_value(field_name)
-
31
return true if optional?(value)
-
-
22
if target.model.properties.named?(field_name)
-
return true unless target.attribute_dirty?(field_name)
-
end
-
-
22
confirm_value = target.instance_variable_get("@#{@confirm_field_name}")
-
22
value == confirm_value
-
end
-
-
end # class ConfirmationValidator
-
-
1
module ValidatesConfirmation
-
1
extend Deprecate
-
-
##
-
# Validates that the given attribute is confirmed by another
-
# attribute. A common use case scenario is when you require a user to
-
# confirm their password, for which you use both password and
-
# password_confirmation attributes.
-
#
-
# @option [Boolean] :allow_nil (true)
-
# true or false.
-
#
-
# @option [Boolean] :allow_blank (true)
-
# true or false.
-
#
-
# @option [Symbol] :confirm (firstattr_confirmation)
-
# The attribute that you want to validate against.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :password, String
-
# property :email, String
-
# attr_accessor :password_confirmation
-
# attr_accessor :email_repeated
-
#
-
# validates_confirmation_of :password
-
# validates_confirmation_of :email, :confirm => :email_repeated
-
#
-
# # a call to valid? will return false unless:
-
# # password == password_confirmation
-
# # and
-
# # email == email_repeated
-
#
-
1
def validates_confirmation_of(*fields)
-
1
validators.add(ConfirmationValidator, *fields)
-
end
-
-
1
deprecate :validates_is_confirmed, :validates_confirmation_of
-
-
end # module ValidatesIsConfirmed
-
end # module Validations
-
end # module DataMapper
-
#require File.dirname(__FILE__) + '/formats/email'
-
-
1
require 'pathname'
-
1
require 'dm-validations/formats/email'
-
1
require 'dm-validations/formats/url'
-
-
1
module DataMapper
-
1
module Validations
-
1
class UnknownValidationFormat < ::ArgumentError; end
-
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class FormatValidator < GenericValidator
-
-
1
FORMATS = {}
-
-
1
include DataMapper::Validations::Format::Email
-
1
include DataMapper::Validations::Format::Url
-
-
1
def initialize(field_name, options = {})
-
1
super
-
-
1
set_optional_by_default
-
end
-
-
1
def call(target)
-
31
return true if valid?(target)
-
-
value = target.validation_property_value(field_name)
-
-
error_message = (
-
@options[:message] || ValidationErrors.default_error_message(
-
:invalid, field_name
-
)
-
)
-
-
add_error(
-
target,
-
error_message.try_call(humanized_field_name, value),
-
field_name
-
)
-
false
-
end
-
-
1
private
-
-
1
def valid?(target)
-
31
value = target.validation_property_value(field_name)
-
31
return true if optional?(value)
-
-
29
validation = @options[:as] || @options[:with]
-
-
29
if validation.is_a?(Symbol) && !FORMATS.has_key?(validation)
-
raise("No such predefined format '#{validation}'")
-
end
-
-
29
validator = if validation.is_a?(Symbol)
-
29
FORMATS[validation][0]
-
else
-
validation
-
end
-
-
29
case validator
-
when Proc then validator.call(value)
-
29
when Regexp then value ? value.to_s =~ validator : false
-
else
-
raise(UnknownValidationFormat, "Can't determine how to validate #{target.class}##{field_name} with #{validator.inspect}")
-
end
-
rescue Encoding::CompatibilityError
-
# This is to work around a bug in jruby - see formats/email.rb
-
false
-
end
-
-
end # class FormatValidator
-
-
1
module ValidatesFormat
-
1
extend Deprecate
-
-
# Validates that the attribute is in the specified format. You may
-
# use the :as (or :with, it's an alias) option to specify the
-
# pre-defined format that you want to validate against. You may also
-
# specify your own format via a Proc or Regexp passed to the the :as
-
# or :with options.
-
#
-
# @option [Boolean] :allow_nil (true)
-
# true or false.
-
#
-
# @option [Boolean] :allow_blank (true)
-
# true or false.
-
#
-
# @option [Format, Proc, Regexp] :as
-
# The pre-defined format, Proc or Regexp to validate against.
-
#
-
# @option [Format, Proc, Regexp] :with
-
# An alias for :as.
-
#
-
# :email_address (format is specified in DataMapper::Validations::Format::Email - note that unicode emails will *not* be matched under MRI1.8.7)
-
# :url (format is specified in DataMapper::Validations::Format::Url)
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :email, String
-
# property :zip_code, String
-
#
-
# validates_format_of :email, :as => :email_address
-
# validates_format_of :zip_code, :with => /^\d{5}$/
-
#
-
# # a call to valid? will return false unless:
-
# # email is formatted like an email address
-
# # and
-
# # zip_code is a string of 5 digits
-
#
-
1
def validates_format_of(*fields)
-
1
validators.add(FormatValidator, *fields)
-
end
-
-
1
deprecate :validates_format, :validates_format_of
-
end # module ValidatesFormat
-
end # module Validations
-
end # module DataMapper
-
# -*- coding: utf-8 -*-
-
1
module DataMapper
-
1
module Validations
-
# All validators extend this base class. Validators must:
-
#
-
# * Implement the initialize method to capture its parameters, also
-
# calling super to have this parent class capture the optional,
-
# general :if and :unless parameters.
-
# * Implement the call method, returning true or false. The call method
-
# provides the validation logic.
-
#
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class GenericValidator
-
-
1
attr_accessor :if_clause, :unless_clause
-
1
attr_reader :field_name, :options, :humanized_field_name
-
-
# Construct a validator. Capture the :if and :unless clauses when
-
# present.
-
#
-
# @param [String, Symbol] field
-
# The property specified for validation.
-
#
-
# @option [Symbol, Proc] :if
-
# The name of a method or a Proc to call to determine if the
-
# validation should occur.
-
#
-
# @option [Symbol, Proc] :unless
-
# The name of a method or a Proc to call to determine if the
-
# validation should not occur.
-
#
-
# @note
-
# All additional key/value pairs are passed through to the validator
-
# that is sub-classing this GenericValidator
-
#
-
1
def initialize(field_name, options = {})
-
46
@field_name = field_name
-
46
@options = DataMapper::Ext::Hash.except(options, :if, :unless)
-
46
@if_clause = options[:if]
-
46
@unless_clause = options[:unless]
-
46
@humanized_field_name = DataMapper::Inflector.humanize(@field_name)
-
end
-
-
# Add an error message to a target resource. If the error corresponds
-
# to a specific field of the resource, add it to that field,
-
# otherwise add it as a :general message.
-
#
-
# @param [Object] target
-
# The resource that has the error.
-
#
-
# @param [String] message
-
# The message to add.
-
#
-
# @param [Symbol] field_name
-
# The name of the field that caused the error.
-
#
-
1
def add_error(target, message, field_name = :general)
-
# TODO: should the field_name for a general message be :default???
-
24
target.errors.add(field_name, message)
-
end
-
-
# Call the validator. "call" is used so the operation is BoundMethod
-
# and Block compatible. This must be implemented in all concrete
-
# classes.
-
#
-
# @param [Object] target
-
# The resource that the validator must be called against.
-
#
-
# @return [Boolean]
-
# true if valid, otherwise false.
-
#
-
1
def call(target)
-
raise NotImplementedError, "#{self.class}#call must be implemented"
-
end
-
-
# Determines if this validator should be run against the
-
# target by evaluating the :if and :unless clauses
-
# optionally passed while specifying any validator.
-
#
-
# @param [Object] target
-
# The resource that we check against.
-
#
-
# @return [Boolean]
-
# true if should be run, otherwise false.
-
#
-
# @api private
-
1
def execute?(target)
-
689
if unless_clause = self.unless_clause
-
!evaluate_conditional_clause(target, unless_clause)
-
689
elsif if_clause = self.if_clause
-
evaluate_conditional_clause(target, if_clause)
-
else
-
689
true
-
end
-
end
-
-
# @api private
-
1
def evaluate_conditional_clause(target, clause)
-
if clause.kind_of?(Symbol)
-
target.__send__(clause)
-
elsif clause.respond_to?(:call)
-
clause.call(target)
-
end
-
end
-
-
# Set the default value for allow_nil and allow_blank
-
#
-
# @param [Boolean] default value
-
#
-
# @api private
-
1
def set_optional_by_default(default = true)
-
5
[ :allow_nil, :allow_blank ].each do |key|
-
10
@options[key] = true unless options.key?(key)
-
end
-
end
-
-
# Test the value to see if it is blank or nil, and if it is allowed.
-
# Note that allowing blank without explicitly denying nil allows nil
-
# values, since nil.blank? is true.
-
#
-
# @param [Object] value
-
# The value to test.
-
#
-
# @return [Boolean]
-
# true if blank/nil is allowed, and the value is blank/nil.
-
#
-
# @api private
-
1
def optional?(value)
-
354
if value.nil?
-
@options[:allow_nil] ||
-
69
(@options[:allow_blank] && !@options.has_key?(:allow_nil))
-
285
elsif DataMapper::Ext.blank?(value)
-
6
@options[:allow_blank]
-
end
-
end
-
-
# Returns true if validators are equal
-
#
-
# Note that this intentionally do
-
# validate options equality
-
#
-
# even though it is hard to imagine a situation
-
# when multiple validations will be used
-
# on the same field with the same conditions
-
# but different options,
-
# it happens to be the case every once in a while
-
# with inferred validations for strings/text and
-
# explicitly given validations with different option
-
# (usually as Range vs. max limit for inferred validation)
-
#
-
# @api semipublic
-
1
def ==(other)
-
self.class == other.class &&
-
304
self.field_name == other.field_name &&
-
self.if_clause == other.if_clause &&
-
self.unless_clause == other.unless_clause &&
-
self.options == other.options
-
end
-
-
1
def inspect
-
"<##{self.class.name} @field_name='#{@field_name}' @if_clause=#{@if_clause.inspect} @unless_clause=#{@unless_clause.inspect} @options=#{@options.inspect}>"
-
end
-
-
1
alias_method :to_s, :inspect
-
-
1
private
-
-
# Get the corresponding Resource property, if it exists.
-
#
-
# Note: DataMapper validations can be used on non-DataMapper resources.
-
# In such cases, the return value will be nil.
-
#
-
# @api private
-
1
def get_resource_property(resource, property_name)
-
335
model = resource.model if resource.respond_to?(:model)
-
335
repository = resource.repository if model
-
335
properties = model.properties(repository.name) if model
-
335
properties[property_name] if properties
-
end
-
-
end # class GenericValidator
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
1
class LengthValidator < GenericValidator
-
-
# Initialize a length validator
-
#
-
# @param [Symbol] field_name
-
# the name of the field to validate
-
#
-
# @param [Hash] options
-
# the validator options
-
#
-
# @api semipublic
-
1
def initialize(field_name, options)
-
7
super
-
-
7
@equal = options[:is] || options[:equals]
-
7
@range = options[:within] || options[:in]
-
7
@min = options[:minimum] || options[:min]
-
7
@max = options[:maximum] || options[:max]
-
-
7
if @min && @max
-
@range ||= @min..@max
-
end
-
end
-
-
# Test the resource field for validity
-
#
-
# @example when the resource field is valid
-
# validator.call(valid_resource) # => true
-
#
-
# @example when the resource field is not valid
-
# validator.call(invalid_resource) # => false
-
#
-
#
-
# @param [Resource] target
-
# the Resource to test
-
#
-
# @return [Boolean]
-
# true if the field is valid, false if not
-
#
-
# @api semipublic
-
1
def call(target)
-
151
value = target.validation_property_value(field_name)
-
151
return true if optional?(value)
-
-
143
return true unless error_message = error_message_for(value)
-
-
add_error(target, error_message, field_name)
-
false
-
end
-
-
1
private
-
-
# Return the error messages for the value if it is invalid
-
#
-
# @param [#to_s] value
-
# the value to test
-
#
-
# @return [String, nil]
-
# the error message if invalid, nil if not
-
#
-
# @api private
-
1
def error_message_for(value)
-
143
if error_message = send(validation_method, value_length(value.to_s))
-
@options.fetch(:message, error_message)
-
end
-
end
-
-
# Return the method to validate the value with
-
#
-
# @return [Symbol]
-
# the validation method
-
#
-
# @api private
-
1
def validation_method
-
@validation_method ||=
-
7
if @equal then :validate_equals
-
7
elsif @range then :validate_range
-
7
elsif @min then :validate_min
-
7
elsif @max then :validate_max
-
143
end
-
end
-
-
# Return the length in characters
-
#
-
# @param [#to_str] value
-
# the string to get the number of characters for
-
#
-
# @return [Integer]
-
# the number of characters in the string
-
#
-
# @api private
-
1
def value_length(value)
-
143
value.to_str.length
-
end
-
-
1
if RUBY_VERSION < '1.9'
-
def value_length(value)
-
value.to_str.scan(/./u).size
-
end
-
end
-
-
# Validate the value length is equal to the expected length
-
#
-
# @param [Integer] length
-
# the value length
-
#
-
# @return [String, nil]
-
# the error message if invalid, nil if not
-
#
-
# @api private
-
1
def validate_equals(length)
-
return if length == @equal
-
-
ValidationErrors.default_error_message(
-
:wrong_length,
-
humanized_field_name,
-
@equal
-
)
-
end
-
-
# Validate the value length is within expected range
-
#
-
# @param [Integer] length
-
# the value length
-
#
-
# @return [String, nil]
-
# the error message if invalid, nil if not
-
#
-
# @api private
-
1
def validate_range(length)
-
return if @range.include?(length)
-
-
ValidationErrors.default_error_message(
-
:length_between,
-
humanized_field_name,
-
@range.min,
-
@range.max
-
)
-
end
-
-
# Validate the minimum expected value length
-
#
-
# @param [Integer] length
-
# the value length
-
#
-
# @return [String, nil]
-
# the error message if invalid, nil if not
-
#
-
# @api private
-
1
def validate_min(length)
-
return if length >= @min
-
-
ValidationErrors.default_error_message(
-
:too_short,
-
humanized_field_name,
-
@min
-
)
-
end
-
-
# Validate the maximum expected value length
-
#
-
# @param [Integer] length
-
# the value length
-
#
-
# @return [String, nil]
-
# the error message if invalid, nil if not
-
#
-
# @api private
-
1
def validate_max(length)
-
143
return if length <= @max
-
-
ValidationErrors.default_error_message(
-
:too_long,
-
humanized_field_name,
-
@max
-
)
-
end
-
-
end # class LengthValidator
-
-
1
module ValidatesLength
-
1
extend Deprecate
-
-
# Validates that the length of the attribute is equal to, less than,
-
# greater than or within a certain range (depending upon the options
-
# you specify).
-
#
-
# @option [Boolean] :allow_nil (true)
-
# true or false.
-
#
-
# @option [Boolean] :allow_blank (true)
-
# true or false.
-
#
-
# @option [Boolean] :minimum
-
# Ensures that the attribute's length is greater than or equal to
-
# the supplied value.
-
#
-
# @option [Boolean] :min
-
# Alias for :minimum.
-
#
-
# @option [Boolean] :maximum
-
# Ensures the attribute's length is less than or equal to the
-
# supplied value.
-
#
-
# @option [Boolean] :max
-
# Alias for :maximum.
-
#
-
# @option [Boolean] :equals
-
# Ensures the attribute's length is equal to the supplied value.
-
#
-
# @option [Boolean] :is
-
# Alias for :equals.
-
#
-
# @option [Range] :in
-
# Given a Range, ensures that the attributes length is include?'ed
-
# in the Range.
-
#
-
# @option [Range] :within
-
# Alias for :in.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property high, Integer
-
# property low, Integer
-
# property just_right, Integer
-
#
-
# validates_length_of :high, :min => 100000000000
-
# validates_length_of :low, :equals => 0
-
# validates_length_of :just_right, :within => 1..10
-
#
-
# # a call to valid? will return false unless:
-
# # high is greater than or equal to 100000000000
-
# # low is equal to 0
-
# # just_right is between 1 and 10 (inclusive of both 1 and 10)
-
#
-
1
def validates_length_of(*fields)
-
7
validators.add(LengthValidator, *fields)
-
end
-
-
1
deprecate :validates_length, :validates_length_of
-
end # module ValidatesLength
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class MethodValidator < GenericValidator
-
-
1
def initialize(field_name, options={})
-
super
-
@options[:method] = @field_name unless @options.key?(:method)
-
end
-
-
1
def call(target)
-
result, message = target.__send__(@options[:method])
-
add_error(target, message, field_name) unless result
-
result
-
end
-
-
1
def ==(other)
-
@options[:method] == other.instance_variable_get(:@options)[:method] && super
-
end
-
-
end # class MethodValidator
-
-
1
module ValidatesWithMethod
-
# Validate using method called on validated object. The method must
-
# to return either true, or a pair of [false, error message string],
-
# and is specified as a symbol passed with :method option.
-
#
-
# This validator does support multiple fields being specified at a
-
# time, but we encourage you to use it with one property/method at a
-
# time.
-
#
-
# Real world experience shows that method validation is often useful
-
# when attribute needs to be virtual and not a property name.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :zip_code, String
-
#
-
# validates_with_method :zip_code,
-
# :method => :in_the_right_location?
-
#
-
# def in_the_right_location?
-
# if @zip_code == "94301"
-
# return true
-
# else
-
# return [false, "You're in the wrong zip code"]
-
# end
-
# end
-
#
-
# # A call to valid? will return false and
-
# # populate the object's errors with "You're in the
-
# # wrong zip code" unless zip_code == "94301"
-
# end
-
1
def validates_with_method(*fields)
-
validators.add(MethodValidator, *fields)
-
end
-
end # module ValidatesWithMethod
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class NumericalityValidator < GenericValidator
-
-
1
def call(target)
-
70
value = target.validation_property_value(field_name)
-
70
return true if optional?(value)
-
-
18
errors = []
-
-
18
validate_with(integer_only? ? :integer : :numeric, value, errors)
-
-
18
add_errors(target, errors)
-
-
# if the number is invalid, skip further tests
-
18
return false if errors.any?
-
-
18
[ :gt, :lt, :gte, :lte, :eq, :ne ].each do |validation_type|
-
108
validate_with(validation_type, value, errors)
-
end
-
-
18
add_errors(target, errors)
-
-
18
errors.empty?
-
end
-
-
1
private
-
-
1
def integer_only?
-
18
options[:only_integer] || options.fetch(:integer_only, false)
-
end
-
-
1
def value_as_string(value)
-
18
case value
-
# Avoid Scientific Notation in Float to_s
-
when Float then value.to_d.to_s('F')
-
when BigDecimal then value.to_s('F')
-
18
else value.to_s
-
end
-
end
-
-
1
def add_errors(target, errors)
-
36
return if errors.empty?
-
-
if options.key?(:message)
-
add_error(target, options[:message], field_name)
-
else
-
errors.each do |error_message|
-
add_error(target, error_message, field_name)
-
end
-
end
-
end
-
-
1
def validate_with(validation_type, value, errors)
-
126
send("validate_#{validation_type}", value, errors)
-
end
-
-
1
def validate_with_comparison(value, cmp, expected, error_message_name, errors, negated = false)
-
126
return if expected.nil?
-
-
# XXX: workaround for jruby. This is needed because the jruby
-
# compiler optimizes a bit too far with magic variables like $~.
-
# the value.send line sends $~. Inserting this line makes sure the
-
# jruby compiler does not optimise here.
-
# see http://jira.codehaus.org/browse/JRUBY-3765
-
36
$~ = nil if RUBY_PLATFORM[/java/]
-
-
36
comparison = value.send(cmp, expected)
-
36
return if negated ? !comparison : comparison
-
-
errors << ValidationErrors.default_error_message(
-
error_message_name,
-
field_name,
-
expected
-
)
-
end
-
-
1
def validate_integer(value, errors)
-
18
validate_with_comparison(value_as_string(value), :=~, /\A[+-]?\d+\z/, :not_an_integer, errors)
-
end
-
-
1
def validate_numeric(value, errors)
-
precision = options[:precision]
-
scale = options[:scale]
-
-
regexp = if precision && scale
-
if precision > scale && scale == 0
-
/\A[+-]?(?:\d{1,#{precision}}(?:\.0)?)\z/
-
elsif precision > scale
-
/\A[+-]?(?:\d{1,#{precision - scale}}|\d{0,#{precision - scale}}\.\d{1,#{scale}})\z/
-
elsif precision == scale
-
/\A[+-]?(?:0(?:\.\d{1,#{scale}})?)\z/
-
else
-
raise ArgumentError, "Invalid precision #{precision.inspect} and scale #{scale.inspect} for #{field_name} (value: #{value.inspect} #{value.class})"
-
end
-
else
-
/\A[+-]?(?:\d+|\d*\.\d+)\z/
-
end
-
-
validate_with_comparison(value_as_string(value), :=~, regexp, :not_a_number, errors)
-
end
-
-
1
def validate_gt(value, errors)
-
18
validate_with_comparison(value, :>, options[:gt] || options[:greater_than], :greater_than, errors)
-
end
-
-
1
def validate_lt(value, errors)
-
18
validate_with_comparison(value, :<, options[:lt] || options[:less_than], :less_than, errors)
-
end
-
-
1
def validate_gte(value, errors)
-
18
validate_with_comparison(value, :>=, options[:gte] || options[:greater_than_or_equal_to], :greater_than_or_equal_to, errors)
-
end
-
-
1
def validate_lte(value, errors)
-
18
validate_with_comparison(value, :<=, options[:lte] || options[:less_than_or_equal_to], :less_than_or_equal_to, errors)
-
end
-
-
1
def validate_eq(value, errors)
-
18
eq = options[:eq] || options[:equal] || options[:equals] || options[:exactly] || options[:equal_to]
-
18
validate_with_comparison(value, :==, eq, :equal_to, errors)
-
end
-
-
1
def validate_ne(value, errors)
-
18
validate_with_comparison(value, :==, options[:ne] || options[:not_equal_to], :not_equal_to, errors, true)
-
end
-
-
end # class NumericalityValidator
-
-
1
module ValidatesNumericality
-
1
extend Deprecate
-
-
# Validate whether a field is numeric.
-
#
-
# @option [Boolean] :allow_nil
-
# true if number can be nil, false if not.
-
#
-
# @option [Boolean] :allow_blank
-
# true if number can be blank, false if not.
-
#
-
# @option [String] :message
-
# Custom error message, also can be a callable object that takes
-
# an object (for pure Ruby objects) or object and property
-
# (for DM resources).
-
#
-
# @option [Numeric] :precision
-
# Required precision of a value.
-
#
-
# @option [Numeric] :scale
-
# Required scale of a value.
-
#
-
# @option [Numeric] :gte
-
# 'Greater than or equal to' requirement.
-
#
-
# @option [Numeric] :lte
-
# 'Less than or equal to' requirement.
-
#
-
# @option [Numeric] :lt
-
# 'Less than' requirement.
-
#
-
# @option [Numeric] :gt
-
# 'Greater than' requirement.
-
#
-
# @option [Numeric] :eq
-
# 'Equal' requirement.
-
#
-
# @option [Numeric] :ne
-
# 'Not equal' requirement.
-
#
-
# @option [Boolean] :integer_only
-
# Use to restrict allowed values to integers.
-
#
-
1
def validates_numericality_of(*fields)
-
11
validators.add(NumericalityValidator, *fields)
-
end
-
-
1
deprecate :validates_is_number, :validates_numericality_of
-
end # module ValidatesIsNumber
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Dirkjan Bussink
-
# @since 0.9
-
1
class PrimitiveTypeValidator < GenericValidator
-
-
1
def call(target)
-
163
value = target.validation_property_value(field_name)
-
163
property = get_resource_property(target, field_name)
-
-
163
return true if value.nil? || property.primitive?(value)
-
-
error_message = @options[:message] || default_error(property)
-
add_error(target, error_message, field_name)
-
-
false
-
end
-
-
1
protected
-
-
1
def default_error(property)
-
ValidationErrors.default_error_message(
-
:primitive,
-
field_name,
-
property.primitive
-
)
-
end
-
-
end # class PrimitiveTypeValidator
-
-
1
module ValidatesPrimitiveType
-
1
extend Deprecate
-
-
# Validates that the specified attribute is of the correct primitive
-
# type.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Person
-
# include DataMapper::Resource
-
#
-
# property :birth_date, Date
-
#
-
# validates_primitive_type_of :birth_date
-
#
-
# # a call to valid? will return false unless
-
# # the birth_date is something that can be properly
-
# # casted into a Date object.
-
# end
-
1
def validates_primitive_type_of(*fields)
-
9
validators.add(PrimitiveTypeValidator, *fields)
-
end
-
-
1
deprecate :validates_is_primitive, :validates_primitive_type_of
-
end # module ValidatesPresent
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class PresenceValidator < GenericValidator
-
-
1
def call(target)
-
172
value = target.validation_property_value(field_name)
-
172
property = get_resource_property(target, field_name)
-
172
return true if present?(value, property)
-
-
18
error_message = @options[:message] || default_error(property)
-
18
add_error(target, error_message, field_name)
-
-
18
false
-
end
-
-
1
protected
-
-
# Boolean property types are considered present if non-nil.
-
# Other property types are considered present if non-blank.
-
# Non-properties are considered present if non-blank.
-
1
def present?(value, property)
-
172
boolean_type?(property) ? !value.nil? : !DataMapper::Ext.blank?(value)
-
end
-
-
1
def default_error(property)
-
18
actual = boolean_type?(property) ? :nil : :blank
-
18
ValidationErrors.default_error_message(actual, field_name)
-
end
-
-
# Is the property a boolean property?
-
#
-
# @return [Boolean]
-
# Returns true for Boolean, ParanoidBoolean, TrueClass and other
-
# properties. Returns false for other property types or for
-
# non-properties.
-
1
def boolean_type?(property)
-
190
property ? property.primitive == TrueClass : false
-
end
-
-
end # class PresenceValidator
-
-
1
module ValidatesPresence
-
1
extend Deprecate
-
-
##
-
# Validates that the specified attribute is present.
-
#
-
# For most property types "being present" is the same as being "not
-
# blank" as determined by the attribute's #blank? method. However, in
-
# the case of Boolean, "being present" means not nil; i.e. true or
-
# false.
-
#
-
# @note
-
# dm-core's support lib adds the blank? method to many classes,
-
#
-
# @see lib/dm-core/support/blank.rb (dm-core) for more information.
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Page
-
# include DataMapper::Resource
-
#
-
# property :required_attribute, String
-
# property :another_required, String
-
# property :yet_again, String
-
#
-
# validates_presence_of :required_attribute
-
# validates_presence_of :another_required, :yet_again
-
#
-
# # a call to valid? will return false unless
-
# # all three attributes are !blank?
-
# end
-
1
def validates_presence_of(*fields)
-
14
validators.add(PresenceValidator, *fields)
-
end
-
-
1
deprecate :validates_present, :validates_presence_of
-
end # module ValidatesPresent
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class UniquenessValidator < GenericValidator
-
-
1
include DataMapper::Assertions
-
-
1
def initialize(field_name, options = {})
-
3
if options.has_key?(:scope)
-
assert_kind_of('scope', options[:scope], Array, Symbol)
-
end
-
-
3
super
-
-
3
set_optional_by_default
-
end
-
-
1
def call(target)
-
71
return true if valid?(target)
-
-
4
error_message = @options[:message] || ValidationErrors.default_error_message(:taken, field_name)
-
4
add_error(target, error_message, field_name)
-
-
4
false
-
end
-
-
1
def valid?(target)
-
71
value = target.validation_property_value(field_name)
-
71
return true if optional?(value)
-
-
69
opts = {
-
:fields => target.model.key(target.repository.name),
-
field_name => value,
-
}
-
-
69
Array(@options[:scope]).each { |subject|
-
unless target.respond_to?(subject)
-
raise(ArgumentError,"Could not find property to scope by: #{subject}. Note that :unique does not currently support arbitrarily named groups, for that you should use :unique_index with an explicit validates_uniqueness_of.")
-
end
-
-
opts[subject] = target.__send__(subject)
-
}
-
-
69
resource = DataMapper.repository(target.repository.name) do
-
69
target.model.first(opts)
-
end
-
-
69
return true if resource.nil?
-
20
target.saved? && resource.key == target.key
-
end
-
-
end # class UniquenessValidator
-
-
1
module ValidatesUniqueness
-
1
extend Deprecate
-
-
# Validate the uniqueness of a field
-
#
-
1
def validates_uniqueness_of(*fields)
-
3
validators.add(UniquenessValidator, *fields)
-
end
-
-
1
deprecate :validates_is_unique, :validates_uniqueness_of
-
end # module ValidatesIsUnique
-
end # module Validations
-
end # module DataMapper
-
1
module DataMapper
-
1
module Validations
-
# @author Guy van den Berg
-
# @since 0.9
-
1
class WithinValidator < GenericValidator
-
-
1
def initialize(field_name, options={})
-
super
-
-
@options[:set] = [] unless @options.has_key?(:set)
-
end
-
-
1
def call(target)
-
value = target.validation_property_value(field_name)
-
return true if optional?(value)
-
return true if @options[:set].include?(value)
-
-
n = 1.0/0
-
set = @options[:set]
-
msg = @options[:message]
-
-
if set.is_a?(Range)
-
if set.first != -n && set.last != n
-
error_message = msg || ValidationErrors.default_error_message(:value_between, field_name, set.first, set.last)
-
elsif set.first == -n
-
error_message = msg || ValidationErrors.default_error_message(:less_than_or_equal_to, field_name, set.last)
-
elsif set.last == n
-
error_message = msg || ValidationErrors.default_error_message(:greater_than_or_equal_to, field_name, set.first)
-
end
-
else
-
error_message = msg || ValidationErrors.default_error_message(:inclusion, field_name, set.to_a.join(', '))
-
end
-
-
add_error(target, error_message, field_name)
-
-
false
-
end
-
-
-
end # class WithinValidator
-
-
1
module ValidatesWithin
-
##
-
# Validates that the value of a field is within a range/set.
-
#
-
# This validation is defined by passing a field along with a :set
-
# parameter. The :set can be a Range or any object which responds
-
# to the #include? method (an array, for example).
-
#
-
# @example Usage
-
# require 'dm-validations'
-
#
-
# class Review
-
# include DataMapper::Resource
-
#
-
# STATES = ['new', 'in_progress', 'published', 'archived']
-
#
-
# property :title, String
-
# property :body, String
-
# property :review_state, String
-
# property :rating, Integer
-
#
-
# validates_within :review_state, :set => STATES
-
# validates_within :rating, :set => 1..5
-
#
-
# # a call to valid? will return false unless
-
# # the two properties conform to their sets
-
# end
-
1
def validates_within(*fields)
-
validators.add(WithinValidator, *fields)
-
end
-
end # module ValidatesWithin
-
end # module Validations
-
end # module DataMapper
-
1
require 'data_objects'
-
1
if RUBY_PLATFORM =~ /java/
-
require 'do_jdbc'
-
require 'java'
-
-
module DataObjects
-
module Postgres
-
JDBC_DRIVER = 'org.postgresql.Driver'
-
end
-
end
-
-
begin
-
java.lang.Thread.currentThread.getContextClassLoader().loadClass(DataObjects::Postgres::JDBC_DRIVER, true)
-
rescue java.lang.ClassNotFoundException
-
require 'jdbc/postgres' # the JDBC driver, packaged as a gem
-
Jdbc::Postgres.load_driver if Jdbc::Postgres.respond_to?(:load_driver)
-
end
-
-
# Another way of loading the JDBC Class. This seems to be more reliable
-
# than Class.forName() within the data_objects.Connection Java class,
-
# which is currently not working as expected.
-
java_import DataObjects::Postgres::JDBC_DRIVER
-
-
end
-
-
1
begin
-
1
require 'do_postgres/do_postgres'
-
rescue LoadError
-
if RUBY_PLATFORM =~ /mingw|mswin/ then
-
RUBY_VERSION =~ /(\d+.\d+)/
-
require "do_postgres/#{$1}/do_postgres"
-
else
-
raise
-
end
-
end
-
-
1
require 'do_postgres/version'
-
1
require 'do_postgres/transaction' if RUBY_PLATFORM !~ /java/
-
1
require 'do_postgres/encoding'
-
1
module DataObjects
-
1
module Postgres
-
1
module Encoding
-
1
MAP = {
-
"Big5" => "BIG5",
-
"GB2312" => "EUC_CN",
-
"EUC-JP" => "EUC_JP",
-
"EUC-KR" => "EUC_KR",
-
"EUC-TW" => "EUC_TW",
-
"GB18030" => "GB18030",
-
"GBK" => "GBK",
-
"ISO-8859-5" => "ISO_8859_5",
-
"ISO-8859-6" => "ISO_8859_6",
-
"ISO-8859-7" => "ISO_8859_7",
-
"ISO-8859-8" => "ISO_8859_8",
-
"KOI8-U" => "KOI8",
-
"ISO-8859-1" => "LATIN1",
-
"ISO-8859-2" => "LATIN2",
-
"ISO-8859-3" => "LATIN3",
-
"ISO-8859-4" => "LATIN4",
-
"ISO-8859-9" => "LATIN5",
-
"ISO-8859-10" => "LATIN6",
-
"ISO-8859-13" => "LATIN7",
-
"ISO-8859-14" => "LATIN8",
-
"ISO-8859-15" => "LATIN9",
-
"ISO-8859-16" => "LATIN10",
-
"Emacs-Mule" => "MULE_INTERNAL",
-
"SJIS" => "SJIS",
-
"US-ASCII" => "SQL_ASCII",
-
"CP949" => "UHC",
-
"UTF-8" => "UTF8",
-
"IBM866" => "WIN866",
-
"Windows-874" => "WIN874",
-
"Windows-1250" => "WIN1250",
-
"Windows-1251" => "WIN1251",
-
"Windows-1252" => "WIN1252",
-
"Windows-1256" => "WIN1256",
-
"Windows-1258" => "WIN1258"
-
}
-
end
-
end
-
end
-
-
1
module DataObjects
-
-
1
module Postgres
-
-
1
class Transaction < DataObjects::Transaction
-
-
1
def begin
-
cmd = "BEGIN"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def begin_prepared
-
cmd = "BEGIN"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def commit
-
cmd = "COMMIT"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def commit_prepared
-
cmd = "COMMIT PREPARED '#{id}'"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def rollback
-
cmd = "ROLLBACK"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def rollback_prepared
-
cmd = "ROLLBACK PREPARED '#{id}'"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
1
def prepare
-
cmd = "PREPARE TRANSACTION '#{id}'"
-
connection.create_command(cmd).execute_non_query
-
end
-
-
end
-
-
end
-
-
end
-
1
module DataObjects
-
1
module Postgres
-
1
VERSION = '0.10.17'
-
end
-
end
-
##
-
1
module MIME
-
end
-
-
# The definition of one MIME content-type.
-
#
-
# == Usage
-
# require 'mime/types'
-
#
-
# plaintext = MIME::Types['text/plain'].first
-
# # returns [text/plain, text/plain]
-
# text = plaintext.first
-
# print text.media_type # => 'text'
-
# print text.sub_type # => 'plain'
-
#
-
# puts text.extensions.join(" ") # => 'asc txt c cc h hh cpp'
-
#
-
# puts text.encoding # => 8bit
-
# puts text.binary? # => false
-
# puts text.ascii? # => true
-
# puts text == 'text/plain' # => true
-
# puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip'
-
#
-
# puts MIME::Types.any? { |type|
-
# type.content_type == 'text/plain'
-
# } # => true
-
# puts MIME::Types.all?(&:registered?)
-
# # => false
-
1
class MIME::Type
-
# Reflects a MIME content-type specification that is not correctly
-
# formatted (it isn't +type+/+subtype+).
-
1
class InvalidContentType < ArgumentError
-
# :stopdoc:
-
1
def initialize(type_string)
-
@type_string = type_string
-
end
-
-
1
def to_s
-
"Invalid Content-Type #{@type_string.inspect}"
-
end
-
# :startdoc:
-
end
-
-
# Reflects an unsupported MIME encoding.
-
1
class InvalidEncoding < ArgumentError
-
# :stopdoc:
-
1
def initialize(encoding)
-
@encoding = encoding
-
end
-
-
1
def to_s
-
"Invalid Encoding #{@encoding.inspect}"
-
end
-
# :startdoc:
-
end
-
-
# The released version of the mime-types library.
-
1
VERSION = '3.1'
-
-
1
include Comparable
-
-
# :stopdoc:
-
# TODO verify mime-type character restrictions; I am pretty sure that this is
-
# too wide open.
-
1
MEDIA_TYPE_RE = %r{([-\w.+]+)/([-\w.+]*)}
-
1
I18N_RE = %r{[^[:alnum:]]}
-
1
BINARY_ENCODINGS = %w(base64 8bit)
-
1
ASCII_ENCODINGS = %w(7bit quoted-printable)
-
# :startdoc:
-
-
1
private_constant :MEDIA_TYPE_RE, :I18N_RE, :BINARY_ENCODINGS,
-
:ASCII_ENCODINGS
-
-
# Builds a MIME::Type object from the +content_type+, a MIME Content Type
-
# value (e.g., 'text/plain' or 'applicaton/x-eruby'). The constructed object
-
# is yielded to an optional block for additional configuration, such as
-
# associating extensions and encoding information.
-
#
-
# * When provided a Hash or a MIME::Type, the MIME::Type will be
-
# constructed with #init_with.
-
# * When provided an Array, the MIME::Type will be constructed using
-
# the first element as the content type and the remaining flattened
-
# elements as extensions.
-
# * Otherwise, the content_type will be used as a string.
-
#
-
# Yields the newly constructed +self+ object.
-
1
def initialize(content_type) # :yields self:
-
@friendly = {}
-
@obsolete = @registered = false
-
@preferred_extension = @docs = @use_instead = nil
-
self.extensions = []
-
-
case content_type
-
when Hash
-
init_with(content_type)
-
when Array
-
self.content_type = content_type.shift
-
self.extensions = content_type.flatten
-
when MIME::Type
-
init_with(content_type.to_h)
-
else
-
self.content_type = content_type
-
end
-
-
self.encoding ||= :default
-
self.xrefs ||= {}
-
-
yield self if block_given?
-
end
-
-
# Indicates that a MIME type is like another type. This differs from
-
# <tt>==</tt> because <tt>x-</tt> prefixes are removed for this comparison.
-
1
def like?(other)
-
other = if other.respond_to?(:simplified)
-
MIME::Type.simplified(other.simplified, remove_x_prefix: true)
-
else
-
MIME::Type.simplified(other.to_s, remove_x_prefix: true)
-
end
-
MIME::Type.simplified(simplified, remove_x_prefix: true) == other
-
end
-
-
# Compares the +other+ MIME::Type against the exact content type or the
-
# simplified type (the simplified type will be used if comparing against
-
# something that can be treated as a String with #to_s). In comparisons, this
-
# is done against the lowercase version of the MIME::Type.
-
1
def <=>(other)
-
3928
if other.nil?
-
-1
-
3928
elsif other.respond_to?(:simplified)
-
simplified <=> other.simplified
-
else
-
3928
simplified <=> MIME::Type.simplified(other.to_s)
-
end
-
end
-
-
# Compares the +other+ MIME::Type based on how reliable it is before doing a
-
# normal <=> comparison. Used by MIME::Types#[] to sort types. The
-
# comparisons involved are:
-
#
-
# 1. self.simplified <=> other.simplified (ensures that we
-
# don't try to compare different types)
-
# 2. IANA-registered definitions < other definitions.
-
# 3. Complete definitions < incomplete definitions.
-
# 4. Current definitions < obsolete definitions.
-
# 5. Obselete with use-instead names < obsolete without.
-
# 6. Obsolete use-instead definitions are compared.
-
#
-
# While this method is public, its use is strongly discouraged by consumers
-
# of mime-types. In mime-types 3, this method is likely to see substantial
-
# revision and simplification to ensure current registered content types sort
-
# before unregistered or obsolete content types.
-
1
def priority_compare(other)
-
pc = simplified <=> other.simplified
-
if pc.zero?
-
pc = if (reg = registered?) != other.registered?
-
reg ? -1 : 1 # registered < unregistered
-
elsif (comp = complete?) != other.complete?
-
comp ? -1 : 1 # complete < incomplete
-
elsif (obs = obsolete?) != other.obsolete?
-
obs ? 1 : -1 # current < obsolete
-
elsif obs and ((ui = use_instead) != (oui = other.use_instead))
-
if ui.nil?
-
1
-
elsif oui.nil?
-
-1
-
else
-
ui <=> oui
-
end
-
else
-
0
-
end
-
end
-
-
pc
-
end
-
-
# Returns +true+ if the +other+ object is a MIME::Type and the content types
-
# match.
-
1
def eql?(other)
-
other.kind_of?(MIME::Type) and self == other
-
end
-
-
# Returns the whole MIME content-type string.
-
#
-
# The content type is a presentation value from the MIME type registry and
-
# should not be used for comparison. The case of the content type is
-
# preserved, and extension markers (<tt>x-</tt>) are kept.
-
#
-
# text/plain => text/plain
-
# x-chemical/x-pdb => x-chemical/x-pdb
-
# audio/QCELP => audio/QCELP
-
1
attr_reader :content_type
-
# A simplified form of the MIME content-type string, suitable for
-
# case-insensitive comparison, with any extension markers (<tt>x-</tt)
-
# removed and converted to lowercase.
-
#
-
# text/plain => text/plain
-
# x-chemical/x-pdb => x-chemical/x-pdb
-
# audio/QCELP => audio/qcelp
-
1
attr_reader :simplified
-
# Returns the media type of the simplified MIME::Type.
-
#
-
# text/plain => text
-
# x-chemical/x-pdb => x-chemical
-
# audio/QCELP => audio
-
1
attr_reader :media_type
-
# Returns the media type of the unmodified MIME::Type.
-
#
-
# text/plain => text
-
# x-chemical/x-pdb => x-chemical
-
# audio/QCELP => audio
-
1
attr_reader :raw_media_type
-
# Returns the sub-type of the simplified MIME::Type.
-
#
-
# text/plain => plain
-
# x-chemical/x-pdb => pdb
-
# audio/QCELP => QCELP
-
1
attr_reader :sub_type
-
# Returns the media type of the unmodified MIME::Type.
-
#
-
# text/plain => plain
-
# x-chemical/x-pdb => x-pdb
-
# audio/QCELP => qcelp
-
1
attr_reader :raw_sub_type
-
-
##
-
# The list of extensions which are known to be used for this MIME::Type.
-
# Non-array values will be coerced into an array with #to_a. Array values
-
# will be flattened, +nil+ values removed, and made unique.
-
#
-
# :attr_accessor: extensions
-
1
def extensions
-
1964
@extensions.to_a
-
end
-
-
##
-
1
def extensions=(value) # :nodoc:
-
1964
@extensions = Set[*Array(value).flatten.compact].freeze
-
1964
MIME::Types.send(:reindex_extensions, self)
-
end
-
-
# Merge the +extensions+ provided into this MIME::Type. The extensions added
-
# will be merged uniquely.
-
1
def add_extensions(*extensions)
-
self.extensions += extensions
-
end
-
-
##
-
# The preferred extension for this MIME type. If one is not set and there are
-
# exceptions defined, the first extension will be used.
-
#
-
# When setting #preferred_extensions, if #extensions does not contain this
-
# extension, this will be added to #xtensions.
-
#
-
# :attr_accessor: preferred_extension
-
-
##
-
1
def preferred_extension
-
@preferred_extension || extensions.first
-
end
-
-
##
-
1
def preferred_extension=(value) # :nodoc:
-
add_extensions(value) if value
-
@preferred_extension = value
-
end
-
-
##
-
# The encoding (+7bit+, +8bit+, <tt>quoted-printable</tt>, or +base64+)
-
# required to transport the data of this content type safely across a
-
# network, which roughly corresponds to Content-Transfer-Encoding. A value of
-
# +nil+ or <tt>:default</tt> will reset the #encoding to the
-
# #default_encoding for the MIME::Type. Raises ArgumentError if the encoding
-
# provided is invalid.
-
#
-
# If the encoding is not provided on construction, this will be either
-
# 'quoted-printable' (for text/* media types) and 'base64' for eveything
-
# else.
-
#
-
# :attr_accessor: encoding
-
-
##
-
1
attr_reader :encoding
-
-
##
-
1
def encoding=(enc) # :nodoc:
-
if enc.nil? or enc == :default
-
@encoding = default_encoding
-
elsif BINARY_ENCODINGS.include?(enc) or ASCII_ENCODINGS.include?(enc)
-
@encoding = enc
-
else
-
fail InvalidEncoding, enc
-
end
-
end
-
-
# Returns the default encoding for the MIME::Type based on the media type.
-
1
def default_encoding
-
(@media_type == 'text') ? 'quoted-printable' : 'base64'
-
end
-
-
##
-
# Returns the media type or types that should be used instead of this media
-
# type, if it is obsolete. If there is no replacement media type, or it is
-
# not obsolete, +nil+ will be returned.
-
#
-
# :attr_accessor: use_instead
-
-
##
-
1
def use_instead
-
obsolete? ? @use_instead : nil
-
end
-
-
##
-
1
attr_writer :use_instead
-
-
# Returns +true+ if the media type is obsolete.
-
1
attr_accessor :obsolete
-
1
alias_method :obsolete?, :obsolete
-
-
# The documentation for this MIME::Type.
-
1
attr_accessor :docs
-
-
# A friendly short description for this MIME::Type.
-
#
-
# call-seq:
-
# text_plain.friendly # => "Text File"
-
# text_plain.friendly('en') # => "Text File"
-
1
def friendly(lang = 'en'.freeze)
-
@friendly ||= {}
-
-
case lang
-
when String, Symbol
-
@friendly[lang.to_s]
-
when Array
-
@friendly.update(Hash[*lang])
-
when Hash
-
@friendly.update(lang)
-
else
-
fail ArgumentError,
-
"Expected a language or translation set, not #{lang.inspect}"
-
end
-
end
-
-
# A key suitable for use as a lookup key for translations, such as with
-
# the I18n library.
-
#
-
# call-seq:
-
# text_plain.i18n_key # => "text.plain"
-
# 3gpp_xml.i18n_key # => "application.vnd-3gpp-bsf-xml"
-
# # from application/vnd.3gpp.bsf+xml
-
# x_msword.i18n_key # => "application.word"
-
# # from application/x-msword
-
1
attr_reader :i18n_key
-
-
##
-
# The cross-references list for this MIME::Type.
-
#
-
# :attr_accessor: xrefs
-
-
##
-
1
attr_reader :xrefs
-
-
##
-
1
def xrefs=(x) # :nodoc:
-
MIME::Types::Container.new.merge(x).tap do |xr|
-
xr.each do |k, v|
-
xr[k] = Set[*v] unless v.kind_of? Set
-
end
-
-
@xrefs = xr
-
end
-
end
-
-
# The decoded cross-reference URL list for this MIME::Type.
-
1
def xref_urls
-
xrefs.flat_map { |type, values|
-
name = :"xref_url_for_#{type.tr('-', '_')}"
-
respond_to?(name, true) and xref_map(values, name) or values.to_a
-
}
-
end
-
-
# Indicates whether the MIME type has been registered with IANA.
-
1
attr_accessor :registered
-
1
alias_method :registered?, :registered
-
-
# MIME types can be specified to be sent across a network in particular
-
# formats. This method returns +true+ when the MIME::Type encoding is set
-
# to <tt>base64</tt>.
-
1
def binary?
-
BINARY_ENCODINGS.include?(encoding)
-
end
-
-
# MIME types can be specified to be sent across a network in particular
-
# formats. This method returns +false+ when the MIME::Type encoding is
-
# set to <tt>base64</tt>.
-
1
def ascii?
-
ASCII_ENCODINGS.include?(encoding)
-
end
-
-
# Indicateswhether the MIME type is declared as a signature type.
-
1
attr_accessor :signature
-
1
alias_method :signature?, :signature
-
-
# Returns +true+ if the MIME::Type specifies an extension list,
-
# indicating that it is a complete MIME::Type.
-
1
def complete?
-
!@extensions.empty?
-
end
-
-
# Returns the MIME::Type as a string.
-
1
def to_s
-
content_type
-
end
-
-
# Returns the MIME::Type as a string for implicit conversions. This allows
-
# MIME::Type objects to appear on either side of a comparison.
-
#
-
# 'text/plain' == MIME::Type.new('text/plain')
-
1
def to_str
-
content_type
-
end
-
-
# Converts the MIME::Type to a JSON string.
-
1
def to_json(*args)
-
require 'json'
-
to_h.to_json(*args)
-
end
-
-
# Converts the MIME::Type to a hash. The output of this method can also be
-
# used to initialize a MIME::Type.
-
1
def to_h
-
encode_with({})
-
end
-
-
# Populates the +coder+ with attributes about this record for
-
# serialization. The structure of +coder+ should match the structure used
-
# with #init_with.
-
#
-
# This method should be considered a private implementation detail.
-
1
def encode_with(coder)
-
coder['content-type'] = @content_type
-
coder['docs'] = @docs unless @docs.nil? or @docs.empty?
-
unless @friendly.nil? or @friendly.empty?
-
coder['friendly'] = @friendly
-
end
-
coder['encoding'] = @encoding
-
coder['extensions'] = @extensions.to_a unless @extensions.empty?
-
coder['preferred-extension'] = @preferred_extension if @preferred_extension
-
if obsolete?
-
coder['obsolete'] = obsolete?
-
coder['use-instead'] = use_instead if use_instead
-
end
-
unless xrefs.empty?
-
{}.tap do |hash|
-
xrefs.each do |k, v|
-
hash[k] = v.sort.to_a
-
end
-
coder['xrefs'] = hash
-
end
-
end
-
coder['registered'] = registered?
-
coder['signature'] = signature? if signature?
-
coder
-
end
-
-
# Initialize an empty object from +coder+, which must contain the
-
# attributes necessary for initializing an empty object.
-
#
-
# This method should be considered a private implementation detail.
-
1
def init_with(coder)
-
self.content_type = coder['content-type']
-
self.docs = coder['docs'] || ''
-
self.encoding = coder['encoding']
-
self.extensions = coder['extensions'] || []
-
self.preferred_extension = coder['preferred-extension']
-
self.obsolete = coder['obsolete'] || false
-
self.registered = coder['registered'] || false
-
self.signature = coder['signature']
-
self.xrefs = coder['xrefs'] || {}
-
self.use_instead = coder['use-instead']
-
-
friendly(coder['friendly'] || {})
-
end
-
-
1
def inspect # :nodoc:
-
# We are intentionally lying here because MIME::Type::Columnar is an
-
# implementation detail.
-
"#<MIME::Type: #{self}>"
-
end
-
-
1
class << self
-
# MIME media types are case-insensitive, but are typically presented in a
-
# case-preserving format in the type registry. This method converts
-
# +content_type+ to lowercase.
-
#
-
# In previous versions of mime-types, this would also remove any extension
-
# prefix (<tt>x-</tt>). This is no longer default behaviour, but may be
-
# provided by providing a truth value to +remove_x_prefix+.
-
1
def simplified(content_type, remove_x_prefix: false)
-
5892
simplify_matchdata(match(content_type), remove_x_prefix)
-
end
-
-
# Converts a provided +content_type+ into a translation key suitable for
-
# use with the I18n library.
-
1
def i18n_key(content_type)
-
1964
simplify_matchdata(match(content_type), joiner: '.') { |e|
-
3928
e.gsub!(I18N_RE, '-'.freeze)
-
}
-
end
-
-
# Return a +MatchData+ object of the +content_type+ against pattern of
-
# media types.
-
1
def match(content_type)
-
7856
case content_type
-
when MatchData
-
3928
content_type
-
else
-
3928
MEDIA_TYPE_RE.match(content_type)
-
end
-
end
-
-
1
private
-
-
1
def simplify_matchdata(matchdata, remove_x = false, joiner: '/'.freeze)
-
7856
return nil unless matchdata
-
-
matchdata.captures.map { |e|
-
7856
e.downcase!
-
7856
e.sub!(%r{^x-}, ''.freeze) if remove_x
-
7856
yield e if block_given?
-
7856
e
-
3928
}.join(joiner)
-
end
-
end
-
-
1
private
-
-
1
def content_type=(type_string)
-
1964
match = MEDIA_TYPE_RE.match(type_string)
-
1964
fail InvalidContentType, type_string if match.nil?
-
-
1964
@content_type = type_string
-
1964
@raw_media_type, @raw_sub_type = match.captures
-
1964
@simplified = MIME::Type.simplified(match)
-
1964
@i18n_key = MIME::Type.i18n_key(match)
-
1964
@media_type, @sub_type = MEDIA_TYPE_RE.match(@simplified).captures
-
end
-
-
1
def xref_map(values, helper)
-
values.map { |value| send(helper, value) }
-
end
-
-
1
def xref_url_for_rfc(value)
-
'http://www.iana.org/go/%s'.freeze % value
-
end
-
-
1
def xref_url_for_draft(value)
-
'http://www.iana.org/go/%s'.freeze % value.sub(/\ARFC/, 'draft')
-
end
-
-
1
def xref_url_for_rfc_errata(value)
-
'http://www.rfc-editor.org/errata_search.php?eid=%s'.freeze % value
-
end
-
-
1
def xref_url_for_person(value)
-
'http://www.iana.org/assignments/media-types/media-types.xhtml#%s'.freeze %
-
value
-
end
-
-
1
def xref_url_for_template(value)
-
'http://www.iana.org/assignments/media-types/%s'.freeze % value
-
end
-
end
-
1
require 'mime/type'
-
-
# A version of MIME::Type that works hand-in-hand with a MIME::Types::Columnar
-
# container to load data by columns.
-
#
-
# When a field is has not yet been loaded, that data will be loaded for all
-
# types in the container before forwarding the message to MIME::Type.
-
#
-
# More information can be found in MIME::Types::Columnar.
-
#
-
# MIME::Type::Columnar is *not* intended to be created except by
-
# MIME::Types::Columnar containers.
-
1
class MIME::Type::Columnar < MIME::Type
-
1
def initialize(container, content_type, extensions) # :nodoc:
-
1964
@container = container
-
1964
self.content_type = content_type
-
1964
self.extensions = extensions
-
end
-
-
1
def self.column(*methods, file: nil) # :nodoc:
-
7
file = methods.first unless file
-
-
7
file_method = :"load_#{file}"
-
7
methods.each do |m|
-
21
define_method m do |*args|
-
@container.send(file_method)
-
super(*args)
-
end
-
end
-
end
-
-
1
column :friendly
-
1
column :encoding, :encoding=
-
1
column :docs, :docs=
-
1
column :preferred_extension, :preferred_extension=
-
1
column :obsolete, :obsolete=, :obsolete?, :registered, :registered=,
-
:registered?, :signature, :signature=, :signature?, file: 'flags'
-
1
column :xrefs, :xrefs=, :xref_urls
-
1
column :use_instead, :use_instead=
-
-
1
def encode_with(coder) # :nodoc:
-
@container.send(:load_friendly)
-
@container.send(:load_encoding)
-
@container.send(:load_docs)
-
@container.send(:load_flags)
-
@container.send(:load_use_instead)
-
@container.send(:load_xrefs)
-
@container.send(:load_preferred_extension)
-
super
-
end
-
-
1
class << self
-
1
undef column
-
end
-
end
-
##
-
1
module MIME
-
##
-
1
class Types
-
end
-
end
-
-
1
require 'mime/type'
-
-
# MIME::Types is a registry of MIME types. It is both a class (created with
-
# MIME::Types.new) and a default registry (loaded automatically or through
-
# interactions with MIME::Types.[] and MIME::Types.type_for).
-
#
-
# == The Default mime-types Registry
-
#
-
# The default mime-types registry is loaded automatically when the library
-
# is required (<tt>require 'mime/types'</tt>), but it may be lazily loaded
-
# (loaded on first use) with the use of the environment variable
-
# +RUBY_MIME_TYPES_LAZY_LOAD+ having any value other than +false+. The
-
# initial startup is about 14× faster (~10 ms vs ~140 ms), but the
-
# registry will be loaded at some point in the future.
-
#
-
# The default mime-types registry can also be loaded from a Marshal cache
-
# file specific to the version of MIME::Types being loaded. This will be
-
# handled automatically with the use of a file referred to in the
-
# environment variable +RUBY_MIME_TYPES_CACHE+. MIME::Types will attempt to
-
# load the registry from this cache file (MIME::Type::Cache.load); if it
-
# cannot be loaded (because the file does not exist, there is an error, or
-
# the data is for a different version of mime-types), the default registry
-
# will be loaded from the normal JSON version and then the cache file will
-
# be *written* to the location indicated by +RUBY_MIME_TYPES_CACHE+. Cache
-
# file loads just over 4½× faster (~30 ms vs ~140 ms).
-
# loads.
-
#
-
# Notes:
-
# * The loading of the default registry is *not* atomic; when using a
-
# multi-threaded environment, it is recommended that lazy loading is not
-
# used and mime-types is loaded as early as possible.
-
# * Cache files should be specified per application in a multiprocess
-
# environment and should be initialized during deployment or before
-
# forking to minimize the chance that the multiple processes will be
-
# trying to write to the same cache file at the same time, or that two
-
# applications that are on different versions of mime-types would be
-
# thrashing the cache.
-
# * Unless cache files are preinitialized, the application using the
-
# mime-types cache file must have read/write permission to the cache file.
-
#
-
# == Usage
-
# require 'mime/types'
-
#
-
# plaintext = MIME::Types['text/plain']
-
# print plaintext.media_type # => 'text'
-
# print plaintext.sub_type # => 'plain'
-
#
-
# puts plaintext.extensions.join(" ") # => 'asc txt c cc h hh cpp'
-
#
-
# puts plaintext.encoding # => 8bit
-
# puts plaintext.binary? # => false
-
# puts plaintext.ascii? # => true
-
# puts plaintext.obsolete? # => false
-
# puts plaintext.registered? # => true
-
# puts plaintext == 'text/plain' # => true
-
# puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip'
-
#
-
1
class MIME::Types
-
# The release version of Ruby MIME::Types
-
1
VERSION = MIME::Type::VERSION
-
-
1
include Enumerable
-
-
# Creates a new MIME::Types registry.
-
1
def initialize
-
1
@type_variants = Container.new
-
1
@extension_index = Container.new
-
end
-
-
# Returns the number of known type variants.
-
1
def count
-
@type_variants.values.inject(0) { |a, e| a + e.size }
-
end
-
-
1
def inspect # :nodoc:
-
"#<#{self.class}: #{count} variants, #{@extension_index.count} extensions>"
-
end
-
-
# Iterates through the type variants.
-
1
def each
-
if block_given?
-
@type_variants.each_value { |tv| tv.each { |t| yield t } }
-
else
-
enum_for(:each)
-
end
-
end
-
-
1
@__types__ = nil
-
-
# Returns a list of MIME::Type objects, which may be empty. The optional
-
# flag parameters are <tt>:complete</tt> (finds only complete MIME::Type
-
# objects) and <tt>:registered</tt> (finds only MIME::Types that are
-
# registered). It is possible for multiple matches to be returned for
-
# either type (in the example below, 'text/plain' returns two values --
-
# one for the general case, and one for VMS systems).
-
#
-
# puts "\nMIME::Types['text/plain']"
-
# MIME::Types['text/plain'].each { |t| puts t.to_a.join(", ") }
-
#
-
# puts "\nMIME::Types[/^image/, complete: true]"
-
# MIME::Types[/^image/, :complete => true].each do |t|
-
# puts t.to_a.join(", ")
-
# end
-
#
-
# If multiple type definitions are returned, returns them sorted as
-
# follows:
-
# 1. Complete definitions sort before incomplete ones;
-
# 2. IANA-registered definitions sort before LTSW-recorded
-
# definitions.
-
# 3. Current definitions sort before obsolete ones;
-
# 4. Obsolete definitions with use-instead clauses sort before those
-
# without;
-
# 5. Obsolete definitions use-instead clauses are compared.
-
# 6. Sort on name.
-
1
def [](type_id, complete: false, registered: false)
-
matches = case type_id
-
when MIME::Type
-
@type_variants[type_id.simplified]
-
when Regexp
-
match(type_id)
-
else
-
@type_variants[MIME::Type.simplified(type_id)]
-
end
-
-
prune_matches(matches, complete, registered).sort { |a, b|
-
a.priority_compare(b)
-
}
-
end
-
-
# Return the list of MIME::Types which belongs to the file based on its
-
# filename extension. If there is no extension, the filename will be used
-
# as the matching criteria on its own.
-
#
-
# This will always return a merged, flatten, priority sorted, unique array.
-
#
-
# puts MIME::Types.type_for('citydesk.xml')
-
# => [application/xml, text/xml]
-
# puts MIME::Types.type_for('citydesk.gif')
-
# => [image/gif]
-
# puts MIME::Types.type_for(%w(citydesk.xml citydesk.gif))
-
# => [application/xml, image/gif, text/xml]
-
1
def type_for(filename)
-
Array(filename).flat_map { |fn|
-
@extension_index[fn.chomp.downcase[/\.?([^.]*?)$/, 1]]
-
}.compact.inject(:+).sort { |a, b|
-
a.priority_compare(b)
-
}
-
end
-
1
alias_method :of, :type_for
-
-
# Add one or more MIME::Type objects to the set of known types. If the
-
# type is already known, a warning will be displayed.
-
#
-
# The last parameter may be the value <tt>:silent</tt> or +true+ which
-
# will suppress duplicate MIME type warnings.
-
1
def add(*types)
-
1964
quiet = ((types.last == :silent) or (types.last == true))
-
-
1964
types.each do |mime_type|
-
1964
case mime_type
-
when true, false, nil, Symbol
-
nil
-
when MIME::Types
-
variants = mime_type.instance_variable_get(:@type_variants)
-
add(*variants.values.inject(:+).to_a, quiet)
-
when Array
-
add(*mime_type, quiet)
-
else
-
1964
add_type(mime_type, quiet)
-
end
-
end
-
end
-
-
# Add a single MIME::Type object to the set of known types. If the +type+ is
-
# already known, a warning will be displayed. The +quiet+ parameter may be a
-
# truthy value to suppress that warning.
-
1
def add_type(type, quiet = false)
-
1964
if !quiet and @type_variants[type.simplified].include?(type)
-
MIME::Types.logger.warn <<-warning
-
Type #{type} is already registered as a variant of #{type.simplified}.
-
warning
-
end
-
-
1964
add_type_variant!(type)
-
1964
index_extensions!(type)
-
end
-
-
1
private
-
-
1
def add_type_variant!(mime_type)
-
1964
@type_variants[mime_type.simplified] << mime_type
-
end
-
-
1
def reindex_extensions!(mime_type)
-
1964
return unless @type_variants[mime_type.simplified].include?(mime_type)
-
index_extensions!(mime_type)
-
end
-
-
1
def index_extensions!(mime_type)
-
3280
mime_type.extensions.each { |ext| @extension_index[ext] << mime_type }
-
end
-
-
1
def prune_matches(matches, complete, registered)
-
matches.delete_if { |e| !e.complete? } if complete
-
matches.delete_if { |e| !e.registered? } if registered
-
matches
-
end
-
-
1
def match(pattern)
-
@type_variants.select { |k, _|
-
k =~ pattern
-
}.values.inject(:+)
-
end
-
end
-
-
1
require 'mime/types/cache'
-
1
require 'mime/types/container'
-
1
require 'mime/types/loader'
-
1
require 'mime/types/logger'
-
1
require 'mime/types/_columnar'
-
1
require 'mime/types/registry'
-
1
require 'mime/type/columnar'
-
-
# MIME::Types::Columnar is used to extend a MIME::Types container to load data
-
# by columns instead of from JSON or YAML. Column loads of MIME types loaded
-
# through the columnar store are synchronized with a Mutex.
-
#
-
# MIME::Types::Columnar is not intended to be used directly, but will be added
-
# to an instance of MIME::Types when it is loaded with
-
# MIME::Types::Loader#load_columnar.
-
1
module MIME::Types::Columnar
-
1
LOAD_MUTEX = Mutex.new # :nodoc:
-
-
1
def self.extended(obj) # :nodoc:
-
1
super
-
1
obj.instance_variable_set(:@__mime_data__, [])
-
1
obj.instance_variable_set(:@__files__, Set.new)
-
end
-
-
# Load the first column data file (type and extensions).
-
1
def load_base_data(path) #:nodoc:
-
1
@__root__ = path
-
-
1
each_file_line('content_type', false) do |line|
-
1964
line = line.split
-
1964
content_type = line.shift
-
1964
extensions = line
-
# content_type, *extensions = line.split
-
-
1964
type = MIME::Type::Columnar.new(self, content_type, extensions)
-
1964
@__mime_data__ << type
-
1964
add(type)
-
end
-
-
1
self
-
end
-
-
1
private
-
-
1
def each_file_line(name, lookup = true)
-
1
LOAD_MUTEX.synchronize do
-
1
next if @__files__.include?(name)
-
-
1
i = -1
-
1
column = File.join(@__root__, "mime.#{name}.column")
-
-
1
IO.readlines(column, encoding: 'UTF-8'.freeze).each do |line|
-
1964
line.chomp!
-
-
1964
if lookup
-
type = @__mime_data__[i += 1] or next
-
yield type, line
-
else
-
1964
yield line
-
end
-
end
-
-
1
@__files__ << name
-
end
-
end
-
-
1
def load_encoding
-
each_file_line('encoding') do |type, line|
-
pool ||= {}
-
line.freeze
-
type.instance_variable_set(:@encoding, (pool[line] ||= line))
-
end
-
end
-
-
1
def load_docs
-
each_file_line('docs') do |type, line|
-
type.instance_variable_set(:@docs, opt(line))
-
end
-
end
-
-
1
def load_preferred_extension
-
each_file_line('pext') do |type, line|
-
type.instance_variable_set(:@preferred_extension, opt(line))
-
end
-
end
-
-
1
def load_flags
-
each_file_line('flags') do |type, line|
-
line = line.split
-
type.instance_variable_set(:@obsolete, flag(line.shift))
-
type.instance_variable_set(:@registered, flag(line.shift))
-
type.instance_variable_set(:@signature, flag(line.shift))
-
end
-
end
-
-
1
def load_xrefs
-
each_file_line('xrefs') { |type, line|
-
type.instance_variable_set(:@xrefs, dict(line, array: true))
-
}
-
end
-
-
1
def load_friendly
-
each_file_line('friendly') { |type, line|
-
type.instance_variable_set(:@friendly, dict(line))
-
}
-
end
-
-
1
def load_use_instead
-
each_file_line('use_instead') do |type, line|
-
type.instance_variable_set(:@use_instead, opt(line))
-
end
-
end
-
-
1
def dict(line, array: false)
-
if line == '-'.freeze
-
{}
-
else
-
line.split('|'.freeze).each_with_object({}) { |l, h|
-
k, v = l.split('^'.freeze)
-
v = nil if v.empty?
-
h[k] = array ? Array(v) : v
-
}
-
end
-
end
-
-
1
def arr(line)
-
if line == '-'.freeze
-
[]
-
else
-
line.split('|'.freeze).flatten.compact.uniq
-
end
-
end
-
-
1
def opt(line)
-
line unless line == '-'.freeze
-
end
-
-
1
def flag(line)
-
line == '1'.freeze ? true : false
-
end
-
end
-
1
MIME::Types::Cache = Struct.new(:version, :data) # :nodoc:
-
-
# Caching of MIME::Types registries is advisable if you will be loading
-
# the default registry relatively frequently. With the class methods on
-
# MIME::Types::Cache, any MIME::Types registry can be marshaled quickly
-
# and easily.
-
#
-
# The cache is invalidated on a per-data-version basis; a cache file for
-
# version 3.2015.1118 will not be reused with version 3.2015.1201.
-
1
class << MIME::Types::Cache
-
# Attempts to load the cache from the file provided as a parameter or in
-
# the environment variable +RUBY_MIME_TYPES_CACHE+. Returns +nil+ if the
-
# file does not exist, if the file cannot be loaded, or if the data in
-
# the cache version is different than this version.
-
1
def load(cache_file = nil)
-
1
cache_file ||= ENV['RUBY_MIME_TYPES_CACHE']
-
1
return nil unless cache_file and File.exist?(cache_file)
-
-
cache = Marshal.load(File.binread(cache_file))
-
if cache.version == MIME::Types::Data::VERSION
-
Marshal.load(cache.data)
-
else
-
MIME::Types.logger.warn <<-warning.chomp
-
Could not load MIME::Types cache: invalid version
-
warning
-
nil
-
end
-
rescue => e
-
MIME::Types.logger.warn <<-warning.chomp
-
Could not load MIME::Types cache: #{e}
-
warning
-
return nil
-
end
-
-
# Attempts to save the types provided to the cache file provided.
-
#
-
# If +types+ is not provided or is +nil+, the cache will contain the
-
# current MIME::Types default registry.
-
#
-
# If +cache_file+ is not provided or is +nil+, the cache will be written
-
# to the file specified in the environment variable
-
# +RUBY_MIME_TYPES_CACHE+. If there is no cache file specified either
-
# directly or through the environment, this method will return +nil+
-
1
def save(types = nil, cache_file = nil)
-
1
cache_file ||= ENV['RUBY_MIME_TYPES_CACHE']
-
1
return nil unless cache_file
-
-
types ||= MIME::Types.send(:__types__)
-
-
File.open(cache_file, 'wb') do |f|
-
f.write(
-
Marshal.dump(new(MIME::Types::Data::VERSION, Marshal.dump(types)))
-
)
-
end
-
end
-
end
-
1
require 'set'
-
-
# MIME::Types requires a container Hash with a default values for keys
-
# resulting in an empty array (<tt>[]</tt>), but this cannot be dumped through
-
# Marshal because of the presence of that default Proc. This class exists
-
# solely to satisfy that need.
-
1
class MIME::Types::Container < Hash # :nodoc:
-
1
def initialize
-
2
super
-
3128
self.default_proc = ->(h, k) { h[k] = Set.new }
-
end
-
-
1
def marshal_dump
-
{}.merge(self)
-
end
-
-
1
def marshal_load(hash)
-
self.default_proc = ->(h, k) { h[k] = Set.new }
-
merge!(hash)
-
end
-
-
1
def encode_with(coder)
-
each { |k, v| coder[k] = v.to_a }
-
end
-
-
1
def init_with(coder)
-
self.default_proc = ->(h, k) { h[k] = Set.new }
-
coder.map.each { |k, v| self[k] = Set[*v] }
-
end
-
end
-
# -*- ruby encoding: utf-8 -*-
-
-
##
-
1
module MIME; end
-
##
-
1
class MIME::Types; end
-
-
1
require 'mime/types/data'
-
-
# This class is responsible for initializing the MIME::Types registry from
-
# the data files supplied with the mime-types library.
-
#
-
# The Loader will use one of the following paths:
-
# 1. The +path+ provided in its constructor argument;
-
# 2. The value of ENV['RUBY_MIME_TYPES_DATA']; or
-
# 3. The value of MIME::Types::Data::PATH.
-
#
-
# When #load is called, the +path+ will be searched recursively for all YAML
-
# (.yml or .yaml) files. By convention, there is one file for each media
-
# type (application.yml, audio.yml, etc.), but this is not required.
-
1
class MIME::Types::Loader
-
# The path that will be read for the MIME::Types files.
-
1
attr_reader :path
-
# The MIME::Types container instance that will be loaded. If not provided
-
# at initialization, a new MIME::Types instance will be constructed.
-
1
attr_reader :container
-
-
# Creates a Loader object that can be used to load MIME::Types registries
-
# into memory, using YAML, JSON, or Columnar registry format loaders.
-
1
def initialize(path = nil, container = nil)
-
1
path = path || ENV['RUBY_MIME_TYPES_DATA'] || MIME::Types::Data::PATH
-
1
@container = container || MIME::Types.new
-
1
@path = File.expand_path(path)
-
# begin
-
# require 'mime/lazy_types'
-
# @container.extend(MIME::LazyTypes)
-
# end
-
end
-
-
# Loads a MIME::Types registry from YAML files (<tt>*.yml</tt> or
-
# <tt>*.yaml</tt>) recursively found in +path+.
-
#
-
# It is expected that the YAML objects contained within the registry array
-
# will be tagged as <tt>!ruby/object:MIME::Type</tt>.
-
#
-
# Note that the YAML format is about 2½ times *slower* than the JSON format.
-
#
-
# NOTE: The purpose of this format is purely for maintenance reasons.
-
1
def load_yaml
-
Dir[yaml_path].sort.each do |f|
-
container.add(*self.class.load_from_yaml(f), :silent)
-
end
-
container
-
end
-
-
# Loads a MIME::Types registry from JSON files (<tt>*.json</tt>)
-
# recursively found in +path+.
-
#
-
# It is expected that the JSON objects will be an array of hash objects.
-
# The JSON format is the registry format for the MIME types registry
-
# shipped with the mime-types library.
-
1
def load_json
-
Dir[json_path].sort.each do |f|
-
types = self.class.load_from_json(f)
-
container.add(*types, :silent)
-
end
-
container
-
end
-
-
# Loads a MIME::Types registry from columnar files recursively found in
-
# +path+.
-
1
def load_columnar
-
1
require 'mime/types/columnar' unless defined?(MIME::Types::Columnar)
-
1
container.extend(MIME::Types::Columnar)
-
1
container.load_base_data(path)
-
-
1
container
-
end
-
-
# Loads a MIME::Types registry. Loads from JSON files by default
-
# (#load_json).
-
#
-
# This will load from columnar files (#load_columnar) if <tt>columnar:
-
# true</tt> is provided in +options+ and there are columnar files in +path+.
-
1
def load(options = { columnar: false })
-
1
if options[:columnar] && !Dir[columnar_path].empty?
-
1
load_columnar
-
else
-
load_json
-
end
-
end
-
-
1
class << self
-
# Loads the default MIME::Type registry.
-
1
def load(options = { columnar: false })
-
1
new.load(options)
-
end
-
-
# Loads MIME::Types from a single YAML file.
-
#
-
# It is expected that the YAML objects contained within the registry
-
# array will be tagged as <tt>!ruby/object:MIME::Type</tt>.
-
#
-
# Note that the YAML format is about 2½ times *slower* than the JSON
-
# format.
-
#
-
# NOTE: The purpose of this format is purely for maintenance reasons.
-
1
def load_from_yaml(filename)
-
begin
-
require 'psych'
-
rescue LoadError
-
nil
-
end
-
require 'yaml'
-
YAML.load(read_file(filename))
-
end
-
-
# Loads MIME::Types from a single JSON file.
-
#
-
# It is expected that the JSON objects will be an array of hash objects.
-
# The JSON format is the registry format for the MIME types registry
-
# shipped with the mime-types library.
-
1
def load_from_json(filename)
-
require 'json'
-
JSON.parse(read_file(filename)).map { |type| MIME::Type.new(type) }
-
end
-
-
1
private
-
-
1
def read_file(filename)
-
File.open(filename, 'r:UTF-8:-', &:read)
-
end
-
end
-
-
1
private
-
-
1
def yaml_path
-
File.join(path, '*.y{,a}ml')
-
end
-
-
1
def json_path
-
File.join(path, '*.json')
-
end
-
-
1
def columnar_path
-
1
File.join(path, '*.column')
-
end
-
end
-
# -*- ruby encoding: utf-8 -*-
-
-
1
require 'logger'
-
-
##
-
1
module MIME
-
##
-
1
class Types
-
1
class << self
-
# Configure the MIME::Types logger. This defaults to an instance of a
-
# logger that passes messages (unformatted) through to Kernel#warn.
-
1
attr_accessor :logger
-
end
-
-
1
class WarnLogger < ::Logger #:nodoc:
-
1
class WarnLogDevice < ::Logger::LogDevice #:nodoc:
-
1
def initialize(*)
-
end
-
-
1
def write(m)
-
Kernel.warn(m)
-
end
-
-
1
def close
-
end
-
end
-
-
1
def initialize(_1, _2 = nil, _3 = nil)
-
1
super nil
-
1
@logdev = WarnLogDevice.new
-
1
@formatter = ->(_s, _d, _p, m) { m }
-
end
-
end
-
-
1
self.logger = WarnLogger.new(nil)
-
end
-
end
-
1
class << MIME::Types
-
1
include Enumerable
-
-
##
-
1
def new(*) # :nodoc:
-
1
super.tap do |types|
-
1
__instances__.add types
-
end
-
end
-
-
# MIME::Types#[] against the default MIME::Types registry.
-
1
def [](type_id, complete: false, registered: false)
-
__types__[type_id, complete: complete, registered: registered]
-
end
-
-
# MIME::Types#count against the default MIME::Types registry.
-
1
def count
-
__types__.count
-
end
-
-
# MIME::Types#each against the default MIME::Types registry.
-
1
def each
-
if block_given?
-
__types__.each { |t| yield t }
-
else
-
enum_for(:each)
-
end
-
end
-
-
# MIME::Types#type_for against the default MIME::Types registry.
-
1
def type_for(filename)
-
__types__.type_for(filename)
-
end
-
1
alias_method :of, :type_for
-
-
# MIME::Types#add against the default MIME::Types registry.
-
1
def add(*types)
-
__types__.add(*types)
-
end
-
-
1
private
-
-
1
def lazy_load?
-
1
(lazy = ENV['RUBY_MIME_TYPES_LAZY_LOAD']) && (lazy != 'false')
-
end
-
-
1
def __types__
-
(defined?(@__types__) and @__types__) or load_default_mime_types
-
end
-
-
1
unless private_method_defined?(:load_mode)
-
1
def load_mode
-
1
{ columnar: true }
-
end
-
end
-
-
1
def load_default_mime_types(mode = load_mode)
-
1
@__types__ = MIME::Types::Cache.load
-
1
unless @__types__
-
1
@__types__ = MIME::Types::Loader.load(mode)
-
1
MIME::Types::Cache.save(@__types__)
-
end
-
1
@__types__
-
end
-
-
1
def __instances__
-
1965
@__instances__ ||= Set.new
-
end
-
-
1
def reindex_extensions(type)
-
1964
__instances__.each do |instance|
-
1964
instance.send(:reindex_extensions!, type)
-
end
-
1964
true
-
end
-
end
-
-
##
-
1
class MIME::Types
-
1
load_default_mime_types(load_mode) unless lazy_load?
-
end
-
# frozen_string_literal: true
-
-
1
module MIME
-
1
class Types
-
1
module Data
-
1
VERSION = '3.2016.0521'
-
-
# The path that will be used for loading the MIME::Types data. The
-
# default location is __FILE__/../../../../data, which is where the data
-
# lives in the gem installation of the mime-types-data library.
-
#
-
# The MIME::Types::Loader will load all JSON or columnar files contained
-
# in this path.
-
#
-
# System maintainer note: this is the constant to change when packaging
-
# mime-types for your system. It is recommended that the path be
-
# something like /usr/share/ruby/mime-types/.
-
1
PATH = File.expand_path('../../../../data', __FILE__)
-
end
-
end
-
end
-
1
require 'multi_json/options'
-
1
require 'multi_json/version'
-
1
require 'multi_json/adapter_error'
-
1
require 'multi_json/parse_error'
-
1
require 'multi_json/options_cache'
-
-
1
module MultiJson
-
1
include Options
-
1
extend self
-
-
1
def default_options=(value)
-
Kernel.warn "MultiJson.default_options setter is deprecated\n" \
-
'Use MultiJson.load_options and MultiJson.dump_options instead'
-
-
self.load_options = self.dump_options = value
-
end
-
-
1
def default_options
-
Kernel.warn "MultiJson.default_options is deprecated\n" \
-
'Use MultiJson.load_options or MultiJson.dump_options instead'
-
-
load_options
-
end
-
-
1
%w(cached_options reset_cached_options!).each do |method_name|
-
2
define_method method_name do |*|
-
Kernel.warn "MultiJson.#{method_name} method is deprecated and no longer used."
-
end
-
end
-
-
1
ALIASES = {'jrjackson' => 'jr_jackson'}
-
-
1
REQUIREMENT_MAP = [
-
[:oj, 'oj'],
-
[:yajl, 'yajl'],
-
[:jr_jackson, 'jrjackson'],
-
[:json_gem, 'json/ext'],
-
[:gson, 'gson'],
-
[:json_pure, 'json/pure'],
-
]
-
-
# The default adapter based on what you currently
-
# have loaded and installed. First checks to see
-
# if any adapters are already loaded, then checks
-
# to see which are installed if none are loaded.
-
1
def default_adapter
-
return :oj if defined?(::Oj)
-
return :yajl if defined?(::Yajl)
-
return :jr_jackson if defined?(::JrJackson)
-
return :json_gem if defined?(::JSON::JSON_LOADED)
-
return :gson if defined?(::Gson)
-
-
REQUIREMENT_MAP.each do |adapter, library|
-
begin
-
require library
-
return adapter
-
rescue ::LoadError
-
next
-
end
-
end
-
-
Kernel.warn '[WARNING] MultiJson is using the default adapter (ok_json). ' \
-
'We recommend loading a different JSON library to improve performance.'
-
-
:ok_json
-
end
-
1
alias_method :default_engine, :default_adapter
-
-
# Get the current adapter class.
-
1
def adapter
-
return @adapter if defined?(@adapter) && @adapter
-
-
use nil # load default adapter
-
-
@adapter
-
end
-
1
alias_method :engine, :adapter
-
-
# Set the JSON parser utilizing a symbol, string, or class.
-
# Supported by default are:
-
#
-
# * <tt>:oj</tt>
-
# * <tt>:json_gem</tt>
-
# * <tt>:json_pure</tt>
-
# * <tt>:ok_json</tt>
-
# * <tt>:yajl</tt>
-
# * <tt>:nsjsonserialization</tt> (MacRuby only)
-
# * <tt>:gson</tt> (JRuby only)
-
# * <tt>:jr_jackson</tt> (JRuby only)
-
1
def use(new_adapter)
-
@adapter = load_adapter(new_adapter)
-
ensure
-
OptionsCache.reset
-
end
-
1
alias_method :adapter=, :use
-
1
alias_method :engine=, :use
-
-
1
def load_adapter(new_adapter)
-
case new_adapter
-
when String, Symbol
-
load_adapter_from_string_name new_adapter.to_s
-
when NilClass, FalseClass
-
load_adapter default_adapter
-
when Class, Module
-
new_adapter
-
else
-
fail ::LoadError, new_adapter
-
end
-
rescue ::LoadError => exception
-
raise AdapterError.build(exception)
-
end
-
-
# Decode a JSON string into Ruby.
-
#
-
# <b>Options</b>
-
#
-
# <tt>:symbolize_keys</tt> :: If true, will use symbols instead of strings for the keys.
-
# <tt>:adapter</tt> :: If set, the selected adapter will be used for this call.
-
1
def load(string, options = {})
-
adapter = current_adapter(options)
-
begin
-
adapter.load(string, options)
-
rescue adapter::ParseError => exception
-
raise ParseError.build(exception, string)
-
end
-
end
-
1
alias_method :decode, :load
-
-
1
def current_adapter(options = {})
-
if (new_adapter = options[:adapter])
-
load_adapter(new_adapter)
-
else
-
adapter
-
end
-
end
-
-
# Encodes a Ruby object as JSON.
-
1
def dump(object, options = {})
-
current_adapter(options).dump(object, options)
-
end
-
1
alias_method :encode, :dump
-
-
# Executes passed block using specified adapter.
-
1
def with_adapter(new_adapter)
-
old_adapter = adapter
-
self.adapter = new_adapter
-
yield
-
ensure
-
self.adapter = old_adapter
-
end
-
1
alias_method :with_engine, :with_adapter
-
-
1
private
-
-
1
def load_adapter_from_string_name(name)
-
name = ALIASES.fetch(name, name)
-
require "multi_json/adapters/#{name.downcase}"
-
klass_name = name.to_s.split('_').map(&:capitalize) * ''
-
MultiJson::Adapters.const_get(klass_name)
-
end
-
end
-
1
module MultiJson
-
1
class AdapterError < ArgumentError
-
1
attr_reader :cause
-
-
1
def self.build(original_exception)
-
message = "Did not recognize your adapter specification (#{original_exception.message})."
-
new(message).tap do |exception|
-
exception.instance_eval do
-
@cause = original_exception
-
set_backtrace original_exception.backtrace
-
end
-
end
-
end
-
end
-
end
-
1
module MultiJson
-
1
module Options
-
1
def load_options=(options)
-
OptionsCache.reset
-
@load_options = options
-
end
-
-
1
def dump_options=(options)
-
OptionsCache.reset
-
@dump_options = options
-
end
-
-
1
def load_options(*args)
-
defined?(@load_options) && get_options(@load_options, *args) || default_load_options
-
end
-
-
1
def dump_options(*args)
-
defined?(@dump_options) && get_options(@dump_options, *args) || default_dump_options
-
end
-
-
1
def default_load_options
-
@default_load_options ||= {}
-
end
-
-
1
def default_dump_options
-
@default_dump_options ||= {}
-
end
-
-
1
private
-
-
1
def get_options(options, *args)
-
if options.respond_to?(:call) && options.arity
-
options.arity == 0 ? options[] : options[*args]
-
elsif options.respond_to?(:to_hash)
-
options.to_hash
-
end
-
end
-
end
-
end
-
1
module MultiJson
-
1
module OptionsCache
-
1
extend self
-
-
1
def reset
-
@dump_cache = {}
-
@load_cache = {}
-
end
-
-
1
def fetch(type, key)
-
cache = instance_variable_get("@#{type}_cache")
-
cache.key?(key) ? cache[key] : write(cache, key, &Proc.new)
-
end
-
-
1
private
-
-
# Normally MultiJson is used with a few option sets for both dump/load
-
# methods. When options are generated dynamically though, every call would
-
# cause a cache miss and the cache would grow indefinitely. To prevent
-
# this, we just reset the cache every time the number of keys outgrows
-
# 1000.
-
1
MAX_CACHE_SIZE = 1000
-
-
1
def write(cache, key)
-
cache.clear if cache.length >= MAX_CACHE_SIZE
-
cache[key] = yield
-
end
-
end
-
end
-
1
module MultiJson
-
1
class ParseError < StandardError
-
1
attr_reader :data, :cause
-
-
1
def self.build(original_exception, data)
-
new(original_exception.message).tap do |exception|
-
exception.instance_eval do
-
@cause = original_exception
-
set_backtrace original_exception.backtrace
-
@data = data
-
end
-
end
-
end
-
end
-
-
1
DecodeError = LoadError = ParseError # Legacy support
-
end
-
1
module MultiJson
-
1
class Version
-
1
MAJOR = 1 unless defined? MultiJson::Version::MAJOR
-
1
MINOR = 12 unless defined? MultiJson::Version::MINOR
-
1
PATCH = 1 unless defined? MultiJson::Version::PATCH
-
1
PRE = nil unless defined? MultiJson::Version::PRE
-
-
1
class << self
-
# @return [String]
-
1
def to_s
-
1
[MAJOR, MINOR, PATCH, PRE].compact.join('.')
-
end
-
end
-
end
-
-
1
VERSION = Version.to_s.freeze
-
end
-
# -*- coding: utf-8 -*-
-
# Modify the PATH on windows so that the external DLLs will get loaded.
-
-
1
require 'rbconfig'
-
-
1
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
-
# The line below caused a problem on non-GAE rack environment.
-
# unless defined?(JRuby::Rack::VERSION) || defined?(AppEngine::ApiProxy)
-
#
-
# However, simply cutting defined?(JRuby::Rack::VERSION) off resulted in
-
# an unable-to-load-nokogiri problem. Thus, now, Nokogiri checks the presense
-
# of appengine-rack.jar in $LOAD_PATH. If Nokogiri is on GAE, Nokogiri
-
# should skip loading xml jars. This is because those are in WEB-INF/lib and
-
# already set in the classpath.
-
unless $LOAD_PATH.to_s.include?("appengine-rack")
-
require 'stringio'
-
require 'isorelax.jar'
-
require 'jing.jar'
-
require 'nekohtml.jar'
-
require 'nekodtd.jar'
-
require 'xercesImpl.jar'
-
require 'serializer.jar'
-
require 'xalan.jar'
-
require 'xml-apis.jar'
-
end
-
end
-
-
1
begin
-
1
RUBY_VERSION =~ /(\d+\.\d+)/
-
1
require "nokogiri/#{$1}/nokogiri"
-
rescue LoadError
-
1
require 'nokogiri/nokogiri'
-
end
-
1
require 'nokogiri/version'
-
1
require 'nokogiri/syntax_error'
-
1
require 'nokogiri/xml'
-
1
require 'nokogiri/xslt'
-
1
require 'nokogiri/html'
-
1
require 'nokogiri/decorators/slop'
-
1
require 'nokogiri/css'
-
1
require 'nokogiri/html/builder'
-
-
# Nokogiri parses and searches XML/HTML very quickly, and also has
-
# correctly implemented CSS3 selector support as well as XPath 1.0
-
# support.
-
#
-
# Parsing a document returns either a Nokogiri::XML::Document, or a
-
# Nokogiri::HTML::Document depending on the kind of document you parse.
-
#
-
# Here is an example:
-
#
-
# require 'nokogiri'
-
# require 'open-uri'
-
#
-
# # Get a Nokogiri::HTML:Document for the page we’re interested in...
-
#
-
# doc = Nokogiri::HTML(open('http://www.google.com/search?q=tenderlove'))
-
#
-
# # Do funky things with it using Nokogiri::XML::Node methods...
-
#
-
# ####
-
# # Search for nodes by css
-
# doc.css('h3.r a.l').each do |link|
-
# puts link.content
-
# end
-
#
-
# See Nokogiri::XML::Searchable#css for more information about CSS searching.
-
# See Nokogiri::XML::Searchable#xpath for more information about XPath searching.
-
1
module Nokogiri
-
1
class << self
-
###
-
# Parse an HTML or XML document. +string+ contains the document.
-
1
def parse string, url = nil, encoding = nil, options = nil
-
if string.respond_to?(:read) ||
-
/^\s*<(?:!DOCTYPE\s+)?html[\s>]/i === string[0, 512]
-
# Expect an HTML indicator to appear within the first 512
-
# characters of a document. (<?xml ?> + <?xml-stylesheet ?>
-
# shouldn't be that long)
-
Nokogiri.HTML(string, url, encoding,
-
options || XML::ParseOptions::DEFAULT_HTML)
-
else
-
Nokogiri.XML(string, url, encoding,
-
options || XML::ParseOptions::DEFAULT_XML)
-
end.tap { |doc|
-
yield doc if block_given?
-
}
-
end
-
-
###
-
# Create a new Nokogiri::XML::DocumentFragment
-
1
def make input = nil, opts = {}, &blk
-
if input
-
Nokogiri::HTML.fragment(input).children.first
-
else
-
Nokogiri(&blk)
-
end
-
end
-
-
###
-
# Parse a document and add the Slop decorator. The Slop decorator
-
# implements method_missing such that methods may be used instead of CSS
-
# or XPath. For example:
-
#
-
# doc = Nokogiri::Slop(<<-eohtml)
-
# <html>
-
# <body>
-
# <p>first</p>
-
# <p>second</p>
-
# </body>
-
# </html>
-
# eohtml
-
# assert_equal('second', doc.html.body.p[1].text)
-
#
-
1
def Slop(*args, &block)
-
Nokogiri(*args, &block).slop!
-
end
-
-
1
def install_default_aliases
-
# Make sure to support some popular encoding aliases not known by
-
# all iconv implementations.
-
{
-
'Windows-31J' => 'CP932', # Windows-31J is the IANA registered name of CP932.
-
1
}.each { |alias_name, name|
-
1
EncodingHandler.alias(name, alias_name) if EncodingHandler[alias_name].nil?
-
}
-
end
-
end
-
-
1
Nokogiri.install_default_aliases
-
end
-
-
###
-
# Parser a document contained in +args+. Nokogiri will try to guess what
-
# type of document you are attempting to parse. For more information, see
-
# Nokogiri.parse
-
#
-
# To specify the type of document, use Nokogiri.XML or Nokogiri.HTML.
-
1
def Nokogiri(*args, &block)
-
if block_given?
-
Nokogiri::HTML::Builder.new(&block).doc.root
-
else
-
Nokogiri.parse(*args)
-
end
-
end
-
1
require 'nokogiri/css/node'
-
1
require 'nokogiri/css/xpath_visitor'
-
1
x = $-w
-
1
$-w = false
-
1
require 'nokogiri/css/parser'
-
1
$-w = x
-
-
1
require 'nokogiri/css/tokenizer'
-
1
require 'nokogiri/css/syntax_error'
-
-
1
module Nokogiri
-
1
module CSS
-
1
class << self
-
###
-
# Parse this CSS selector in +selector+. Returns an AST.
-
1
def parse selector
-
Parser.new.parse selector
-
end
-
-
###
-
# Get the XPath for +selector+.
-
1
def xpath_for selector, options={}
-
Parser.new(options[:ns] || {}).xpath_for selector, options
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module CSS
-
1
class Node
-
1
ALLOW_COMBINATOR_ON_SELF = [:DIRECT_ADJACENT_SELECTOR, :FOLLOWING_SELECTOR, :CHILD_SELECTOR]
-
-
# Get the type of this node
-
1
attr_accessor :type
-
# Get the value of this node
-
1
attr_accessor :value
-
-
# Create a new Node with +type+ and +value+
-
1
def initialize type, value
-
@type = type
-
@value = value
-
end
-
-
# Accept +visitor+
-
1
def accept visitor
-
visitor.send(:"visit_#{type.to_s.downcase}", self)
-
end
-
-
###
-
# Convert this CSS node to xpath with +prefix+ using +visitor+
-
1
def to_xpath prefix = '//', visitor = XPathVisitor.new
-
prefix = '.' if ALLOW_COMBINATOR_ON_SELF.include?(type) && value.first.nil?
-
prefix + visitor.accept(self)
-
end
-
-
# Find a node by type using +types+
-
1
def find_by_type types
-
matches = []
-
matches << self if to_type == types
-
@value.each do |v|
-
matches += v.find_by_type(types) if v.respond_to?(:find_by_type)
-
end
-
matches
-
end
-
-
# Convert to_type
-
1
def to_type
-
[@type] + @value.map { |n|
-
n.to_type if n.respond_to?(:to_type)
-
}.compact
-
end
-
-
# Convert to array
-
1
def to_a
-
[@type] + @value.map { |n| n.respond_to?(:to_a) ? n.to_a : [n] }
-
end
-
end
-
end
-
end
-
#
-
# DO NOT MODIFY!!!!
-
# This file is automatically generated by Racc 1.4.12
-
# from Racc grammer file "".
-
#
-
-
1
require 'racc/parser.rb'
-
-
-
1
require 'nokogiri/css/parser_extras'
-
-
1
module Nokogiri
-
1
module CSS
-
1
class Parser < Racc::Parser
-
-
-
1
def unescape_css_identifier(identifier)
-
identifier.gsub(/\\(?:([^0-9a-fA-F])|([0-9a-fA-F]{1,6})\s?)/){ |m| $1 || [$2.hex].pack('U') }
-
end
-
##### State transition tables begin ###
-
-
1
racc_action_table = [
-
24, 93, 56, 57, 33, 55, 94, 23, 24, 22,
-
12, 93, 33, 27, 35, 52, 88, 22, -23, 25,
-
92, 98, 23, 33, 26, 18, 20, 25, 27, 86,
-
23, 24, 26, 18, 20, 33, 27, 11, 39, 24,
-
22, 23, 89, 33, 18, 101, 100, 27, 22, 12,
-
25, 24, 95, 23, 90, 26, 18, 20, 25, 27,
-
66, 23, 24, 26, 18, 20, 33, 27, 91, 90,
-
51, 22, 96, 85, 33, 26, 33, -23, 33, 56,
-
87, 25, 60, 99, 23, 74, 26, 18, 20, 39,
-
27, 39, 23, 39, 23, 18, 23, 18, 27, 18,
-
27, 33, 27, 33, 56, 87, 22, 60, 56, 87,
-
102, 60, 56, 87, 33, 60, 39, 24, 39, 23,
-
103, 23, 18, 20, 18, 27, 46, 27, 49, 39,
-
93, 44, 23, 105, 33, 18, 51, 45, 27, 33,
-
-23, 26, 108, 56, 58, 109, 60, nil, nil, 39,
-
nil, nil, 23, nil, 39, 18, nil, 23, 27, nil,
-
18, 20, nil, 27, 82, 83, nil, nil, nil, 82,
-
83, nil, nil, nil, nil, 78, 79, 80, nil, 81,
-
78, 79, 80, 77, 81, 4, 5, 10, 77, 4,
-
5, 43, nil, nil, nil, 6, nil, 8, 7, 6,
-
nil, 8, 7, 4, 5, 10, nil, nil, nil, nil,
-
nil, nil, nil, 6, nil, 8, 7 ]
-
-
1
racc_action_check = [
-
42, 58, 24, 24, 42, 24, 57, 15, 43, 42,
-
64, 57, 43, 15, 11, 24, 53, 43, 58, 42,
-
56, 64, 42, 14, 42, 42, 42, 43, 42, 50,
-
43, 3, 43, 43, 43, 3, 43, 1, 14, 9,
-
3, 14, 54, 9, 14, 76, 76, 14, 9, 1,
-
3, 27, 59, 3, 60, 3, 3, 3, 9, 3,
-
27, 9, 12, 9, 9, 9, 12, 9, 55, 55,
-
27, 12, 61, 49, 28, 27, 62, 46, 30, 92,
-
92, 12, 92, 75, 12, 45, 12, 12, 12, 28,
-
12, 62, 28, 30, 62, 28, 30, 62, 28, 30,
-
62, 39, 30, 32, 90, 90, 39, 90, 51, 51,
-
84, 51, 93, 93, 31, 93, 39, 23, 32, 39,
-
86, 32, 39, 39, 32, 39, 23, 32, 23, 31,
-
87, 18, 31, 91, 29, 31, 23, 21, 31, 25,
-
22, 23, 94, 25, 25, 105, 25, nil, nil, 29,
-
nil, nil, 29, nil, 25, 29, nil, 25, 29, nil,
-
25, 25, nil, 25, 48, 48, nil, nil, nil, 47,
-
47, nil, nil, nil, nil, 48, 48, 48, nil, 48,
-
47, 47, 47, 48, 47, 0, 0, 0, 47, 17,
-
17, 17, nil, nil, nil, 0, nil, 0, 0, 17,
-
nil, 17, 17, 26, 26, 26, nil, nil, nil, nil,
-
nil, nil, nil, 26, nil, 26, 26 ]
-
-
1
racc_action_pointer = [
-
178, 37, nil, 29, nil, nil, nil, nil, nil, 37,
-
nil, 14, 60, nil, 17, -17, nil, 182, 120, nil,
-
nil, 108, 111, 115, -8, 133, 196, 49, 68, 128,
-
72, 108, 97, nil, nil, nil, nil, nil, nil, 95,
-
nil, nil, -2, 6, nil, 74, 48, 166, 161, 48,
-
0, 98, nil, -7, 19, 57, 8, -1, -11, 29,
-
42, 49, 70, nil, -2, nil, nil, nil, nil, nil,
-
nil, nil, nil, nil, nil, 58, 35, nil, nil, nil,
-
nil, nil, nil, nil, 85, nil, 109, 118, nil, nil,
-
94, 126, 69, 102, 129, nil, nil, nil, nil, nil,
-
nil, nil, nil, nil, nil, 132, nil, nil, nil, nil ]
-
-
1
racc_action_default = [
-
-74, -75, -2, -24, -4, -5, -6, -7, -8, -24,
-
-73, -75, -24, -3, -47, -10, -13, -17, -75, -19,
-
-20, -75, -22, -24, -75, -24, -74, -75, -53, -54,
-
-55, -56, -57, -58, -14, 110, -1, -9, -46, -24,
-
-11, -12, -24, -24, -18, -75, -29, -61, -61, -75,
-
-75, -75, -30, -75, -75, -38, -39, -40, -22, -75,
-
-38, -75, -70, -72, -75, -44, -45, -48, -49, -50,
-
-51, -52, -15, -16, -21, -75, -75, -62, -63, -64,
-
-65, -66, -67, -68, -75, -27, -75, -40, -31, -32,
-
-75, -43, -75, -75, -75, -33, -69, -71, -34, -25,
-
-59, -60, -26, -28, -35, -75, -36, -37, -42, -41 ]
-
-
1
racc_goto_table = [
-
53, 38, 13, 1, 41, 48, 62, 40, 34, 65,
-
50, 36, 63, 75, 84, 67, 68, 69, 70, 71,
-
62, 47, 37, 42, 54, nil, 63, nil, nil, 64,
-
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
-
nil, 72, 73, nil, nil, nil, nil, nil, nil, 97,
-
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
-
nil, nil, nil, nil, nil, nil, 104, nil, 106, 107 ]
-
-
1
racc_goto_check = [
-
18, 12, 2, 1, 11, 9, 7, 10, 2, 9,
-
15, 2, 12, 17, 17, 12, 12, 12, 12, 12,
-
7, 16, 8, 5, 19, nil, 12, nil, nil, 1,
-
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
-
nil, 2, 2, nil, nil, nil, nil, nil, nil, 12,
-
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
-
nil, nil, nil, nil, nil, nil, 18, nil, 18, 18 ]
-
-
1
racc_goto_pointer = [
-
nil, 3, -1, nil, nil, 6, nil, -19, 8, -18,
-
-8, -11, -13, nil, nil, -13, -2, -34, -24, 0,
-
nil, nil, nil, nil ]
-
-
1
racc_goto_default = [
-
nil, nil, nil, 2, 3, 9, 17, 14, nil, 15,
-
31, 30, 16, 29, 19, 21, nil, nil, 59, nil,
-
28, 32, 76, 61 ]
-
-
1
racc_reduce_table = [
-
0, 0, :racc_error,
-
3, 32, :_reduce_1,
-
1, 32, :_reduce_2,
-
2, 32, :_reduce_3,
-
1, 36, :_reduce_4,
-
1, 36, :_reduce_5,
-
1, 36, :_reduce_6,
-
1, 36, :_reduce_7,
-
1, 36, :_reduce_8,
-
2, 37, :_reduce_9,
-
1, 37, :_reduce_none,
-
2, 37, :_reduce_11,
-
2, 37, :_reduce_12,
-
1, 37, :_reduce_13,
-
2, 34, :_reduce_14,
-
3, 33, :_reduce_15,
-
3, 33, :_reduce_16,
-
1, 33, :_reduce_none,
-
2, 44, :_reduce_18,
-
1, 38, :_reduce_none,
-
1, 38, :_reduce_20,
-
3, 45, :_reduce_21,
-
1, 45, :_reduce_22,
-
1, 46, :_reduce_23,
-
0, 46, :_reduce_none,
-
4, 42, :_reduce_25,
-
4, 42, :_reduce_26,
-
3, 42, :_reduce_27,
-
3, 47, :_reduce_28,
-
1, 47, :_reduce_29,
-
2, 40, :_reduce_30,
-
3, 40, :_reduce_31,
-
3, 40, :_reduce_32,
-
3, 40, :_reduce_33,
-
3, 40, :_reduce_34,
-
3, 49, :_reduce_35,
-
3, 49, :_reduce_36,
-
3, 49, :_reduce_37,
-
1, 49, :_reduce_none,
-
1, 49, :_reduce_none,
-
1, 49, :_reduce_40,
-
4, 50, :_reduce_41,
-
3, 50, :_reduce_42,
-
2, 50, :_reduce_43,
-
2, 41, :_reduce_44,
-
2, 41, :_reduce_45,
-
1, 39, :_reduce_none,
-
0, 39, :_reduce_none,
-
2, 43, :_reduce_48,
-
2, 43, :_reduce_49,
-
2, 43, :_reduce_50,
-
2, 43, :_reduce_51,
-
2, 43, :_reduce_52,
-
1, 43, :_reduce_none,
-
1, 43, :_reduce_none,
-
1, 43, :_reduce_none,
-
1, 43, :_reduce_none,
-
1, 43, :_reduce_none,
-
1, 51, :_reduce_58,
-
2, 48, :_reduce_59,
-
2, 48, :_reduce_60,
-
0, 48, :_reduce_none,
-
1, 53, :_reduce_62,
-
1, 53, :_reduce_63,
-
1, 53, :_reduce_64,
-
1, 53, :_reduce_65,
-
1, 53, :_reduce_66,
-
1, 53, :_reduce_67,
-
1, 53, :_reduce_68,
-
3, 52, :_reduce_69,
-
1, 54, :_reduce_none,
-
2, 54, :_reduce_none,
-
1, 54, :_reduce_none,
-
1, 35, :_reduce_none,
-
0, 35, :_reduce_none ]
-
-
1
racc_reduce_n = 75
-
-
1
racc_shift_n = 110
-
-
1
racc_token_table = {
-
false => 0,
-
:error => 1,
-
:FUNCTION => 2,
-
:INCLUDES => 3,
-
:DASHMATCH => 4,
-
:LBRACE => 5,
-
:HASH => 6,
-
:PLUS => 7,
-
:GREATER => 8,
-
:S => 9,
-
:STRING => 10,
-
:IDENT => 11,
-
:COMMA => 12,
-
:NUMBER => 13,
-
:PREFIXMATCH => 14,
-
:SUFFIXMATCH => 15,
-
:SUBSTRINGMATCH => 16,
-
:TILDE => 17,
-
:NOT_EQUAL => 18,
-
:SLASH => 19,
-
:DOUBLESLASH => 20,
-
:NOT => 21,
-
:EQUAL => 22,
-
:RPAREN => 23,
-
:LSQUARE => 24,
-
:RSQUARE => 25,
-
:HAS => 26,
-
"." => 27,
-
"*" => 28,
-
"|" => 29,
-
":" => 30 }
-
-
1
racc_nt_base = 31
-
-
1
racc_use_result_var = true
-
-
1
Racc_arg = [
-
racc_action_table,
-
racc_action_check,
-
racc_action_default,
-
racc_action_pointer,
-
racc_goto_table,
-
racc_goto_check,
-
racc_goto_default,
-
racc_goto_pointer,
-
racc_nt_base,
-
racc_reduce_table,
-
racc_token_table,
-
racc_shift_n,
-
racc_reduce_n,
-
racc_use_result_var ]
-
-
1
Racc_token_to_s_table = [
-
"$end",
-
"error",
-
"FUNCTION",
-
"INCLUDES",
-
"DASHMATCH",
-
"LBRACE",
-
"HASH",
-
"PLUS",
-
"GREATER",
-
"S",
-
"STRING",
-
"IDENT",
-
"COMMA",
-
"NUMBER",
-
"PREFIXMATCH",
-
"SUFFIXMATCH",
-
"SUBSTRINGMATCH",
-
"TILDE",
-
"NOT_EQUAL",
-
"SLASH",
-
"DOUBLESLASH",
-
"NOT",
-
"EQUAL",
-
"RPAREN",
-
"LSQUARE",
-
"RSQUARE",
-
"HAS",
-
"\".\"",
-
"\"*\"",
-
"\"|\"",
-
"\":\"",
-
"$start",
-
"selector",
-
"simple_selector_1toN",
-
"prefixless_combinator_selector",
-
"optional_S",
-
"combinator",
-
"simple_selector",
-
"element_name",
-
"hcap_0toN",
-
"function",
-
"pseudo",
-
"attrib",
-
"hcap_1toN",
-
"class",
-
"namespaced_ident",
-
"namespace",
-
"attrib_name",
-
"attrib_val_0or1",
-
"expr",
-
"nth",
-
"attribute_id",
-
"negation",
-
"eql_incl_dash",
-
"negation_arg" ]
-
-
1
Racc_debug_parser = false
-
-
##### State transition tables end #####
-
-
# reduce 0 omitted
-
-
1
def _reduce_1(val, _values, result)
-
result = [val.first, val.last].flatten
-
-
result
-
end
-
-
1
def _reduce_2(val, _values, result)
-
result = val.flatten
-
result
-
end
-
-
1
def _reduce_3(val, _values, result)
-
result = [val.last].flatten
-
result
-
end
-
-
1
def _reduce_4(val, _values, result)
-
result = :DIRECT_ADJACENT_SELECTOR
-
result
-
end
-
-
1
def _reduce_5(val, _values, result)
-
result = :CHILD_SELECTOR
-
result
-
end
-
-
1
def _reduce_6(val, _values, result)
-
result = :FOLLOWING_SELECTOR
-
result
-
end
-
-
1
def _reduce_7(val, _values, result)
-
result = :DESCENDANT_SELECTOR
-
result
-
end
-
-
1
def _reduce_8(val, _values, result)
-
result = :CHILD_SELECTOR
-
result
-
end
-
-
1
def _reduce_9(val, _values, result)
-
result = if val[1].nil?
-
val.first
-
else
-
Node.new(:CONDITIONAL_SELECTOR, [val.first, val[1]])
-
end
-
-
result
-
end
-
-
# reduce 10 omitted
-
-
1
def _reduce_11(val, _values, result)
-
result = Node.new(:CONDITIONAL_SELECTOR, val)
-
-
result
-
end
-
-
1
def _reduce_12(val, _values, result)
-
result = Node.new(:CONDITIONAL_SELECTOR, val)
-
-
result
-
end
-
-
1
def _reduce_13(val, _values, result)
-
result = Node.new(:CONDITIONAL_SELECTOR,
-
[Node.new(:ELEMENT_NAME, ['*']), val.first]
-
)
-
-
result
-
end
-
-
1
def _reduce_14(val, _values, result)
-
result = Node.new(val.first, [nil, val.last])
-
-
result
-
end
-
-
1
def _reduce_15(val, _values, result)
-
result = Node.new(val[1], [val.first, val.last])
-
-
result
-
end
-
-
1
def _reduce_16(val, _values, result)
-
result = Node.new(:DESCENDANT_SELECTOR, [val.first, val.last])
-
-
result
-
end
-
-
# reduce 17 omitted
-
-
1
def _reduce_18(val, _values, result)
-
result = Node.new(:CLASS_CONDITION, [unescape_css_identifier(val[1])])
-
result
-
end
-
-
# reduce 19 omitted
-
-
1
def _reduce_20(val, _values, result)
-
result = Node.new(:ELEMENT_NAME, val)
-
result
-
end
-
-
1
def _reduce_21(val, _values, result)
-
result = Node.new(:ELEMENT_NAME,
-
[[val.first, val.last].compact.join(':')]
-
)
-
-
result
-
end
-
-
1
def _reduce_22(val, _values, result)
-
name = @namespaces.key?('xmlns') ? "xmlns:#{val.first}" : val.first
-
result = Node.new(:ELEMENT_NAME, [name])
-
-
result
-
end
-
-
1
def _reduce_23(val, _values, result)
-
result = val[0]
-
result
-
end
-
-
# reduce 24 omitted
-
-
1
def _reduce_25(val, _values, result)
-
result = Node.new(:ATTRIBUTE_CONDITION,
-
[val[1]] + (val[2] || [])
-
)
-
-
result
-
end
-
-
1
def _reduce_26(val, _values, result)
-
result = Node.new(:ATTRIBUTE_CONDITION,
-
[val[1]] + (val[2] || [])
-
)
-
-
result
-
end
-
-
1
def _reduce_27(val, _values, result)
-
# Non standard, but hpricot supports it.
-
result = Node.new(:PSEUDO_CLASS,
-
[Node.new(:FUNCTION, ['nth-child(', val[1]])]
-
)
-
-
result
-
end
-
-
1
def _reduce_28(val, _values, result)
-
result = Node.new(:ELEMENT_NAME,
-
[[val.first, val.last].compact.join(':')]
-
)
-
-
result
-
end
-
-
1
def _reduce_29(val, _values, result)
-
# Default namespace is not applied to attributes.
-
# So we don't add prefix "xmlns:" as in namespaced_ident.
-
result = Node.new(:ELEMENT_NAME, [val.first])
-
-
result
-
end
-
-
1
def _reduce_30(val, _values, result)
-
result = Node.new(:FUNCTION, [val.first.strip])
-
-
result
-
end
-
-
1
def _reduce_31(val, _values, result)
-
result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
-
-
result
-
end
-
-
1
def _reduce_32(val, _values, result)
-
result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
-
-
result
-
end
-
-
1
def _reduce_33(val, _values, result)
-
result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
-
-
result
-
end
-
-
1
def _reduce_34(val, _values, result)
-
result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
-
-
result
-
end
-
-
1
def _reduce_35(val, _values, result)
-
result = [val.first, val.last]
-
result
-
end
-
-
1
def _reduce_36(val, _values, result)
-
result = [val.first, val.last]
-
result
-
end
-
-
1
def _reduce_37(val, _values, result)
-
result = [val.first, val.last]
-
result
-
end
-
-
# reduce 38 omitted
-
-
# reduce 39 omitted
-
-
1
def _reduce_40(val, _values, result)
-
case val[0]
-
when 'even'
-
result = Node.new(:NTH, ['2','n','+','0'])
-
when 'odd'
-
result = Node.new(:NTH, ['2','n','+','1'])
-
when 'n'
-
result = Node.new(:NTH, ['1','n','+','0'])
-
else
-
# This is not CSS standard. It allows us to support this:
-
# assert_xpath("//a[foo(., @href)]", @parser.parse('a:foo(@href)'))
-
# assert_xpath("//a[foo(., @a, b)]", @parser.parse('a:foo(@a, b)'))
-
# assert_xpath("//a[foo(., a, 10)]", @parser.parse('a:foo(a, 10)'))
-
result = val
-
end
-
-
result
-
end
-
-
1
def _reduce_41(val, _values, result)
-
if val[1] == 'n'
-
result = Node.new(:NTH, val)
-
else
-
raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
-
end
-
-
result
-
end
-
-
1
def _reduce_42(val, _values, result)
-
# n+3, -n+3
-
if val[0] == 'n'
-
val.unshift("1")
-
result = Node.new(:NTH, val)
-
elsif val[0] == '-n'
-
val[0] = 'n'
-
val.unshift("-1")
-
result = Node.new(:NTH, val)
-
else
-
raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
-
end
-
-
result
-
end
-
-
1
def _reduce_43(val, _values, result)
-
# 5n, -5n, 10n-1
-
n = val[1]
-
if n[0, 2] == 'n-'
-
val[1] = 'n'
-
val << "-"
-
# b is contained in n as n is the string "n-b"
-
val << n[2, n.size]
-
result = Node.new(:NTH, val)
-
elsif n == 'n'
-
val << "+"
-
val << "0"
-
result = Node.new(:NTH, val)
-
else
-
raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
-
end
-
-
result
-
end
-
-
1
def _reduce_44(val, _values, result)
-
result = Node.new(:PSEUDO_CLASS, [val[1]])
-
-
result
-
end
-
-
1
def _reduce_45(val, _values, result)
-
result = Node.new(:PSEUDO_CLASS, [val[1]])
-
result
-
end
-
-
# reduce 46 omitted
-
-
# reduce 47 omitted
-
-
1
def _reduce_48(val, _values, result)
-
result = Node.new(:COMBINATOR, val)
-
-
result
-
end
-
-
1
def _reduce_49(val, _values, result)
-
result = Node.new(:COMBINATOR, val)
-
-
result
-
end
-
-
1
def _reduce_50(val, _values, result)
-
result = Node.new(:COMBINATOR, val)
-
-
result
-
end
-
-
1
def _reduce_51(val, _values, result)
-
result = Node.new(:COMBINATOR, val)
-
-
result
-
end
-
-
1
def _reduce_52(val, _values, result)
-
result = Node.new(:COMBINATOR, val)
-
-
result
-
end
-
-
# reduce 53 omitted
-
-
# reduce 54 omitted
-
-
# reduce 55 omitted
-
-
# reduce 56 omitted
-
-
# reduce 57 omitted
-
-
1
def _reduce_58(val, _values, result)
-
result = Node.new(:ID, [unescape_css_identifier(val.first)])
-
result
-
end
-
-
1
def _reduce_59(val, _values, result)
-
result = [val.first, val[1]]
-
result
-
end
-
-
1
def _reduce_60(val, _values, result)
-
result = [val.first, val[1]]
-
result
-
end
-
-
# reduce 61 omitted
-
-
1
def _reduce_62(val, _values, result)
-
result = :equal
-
result
-
end
-
-
1
def _reduce_63(val, _values, result)
-
result = :prefix_match
-
result
-
end
-
-
1
def _reduce_64(val, _values, result)
-
result = :suffix_match
-
result
-
end
-
-
1
def _reduce_65(val, _values, result)
-
result = :substring_match
-
result
-
end
-
-
1
def _reduce_66(val, _values, result)
-
result = :not_equal
-
result
-
end
-
-
1
def _reduce_67(val, _values, result)
-
result = :includes
-
result
-
end
-
-
1
def _reduce_68(val, _values, result)
-
result = :dash_match
-
result
-
end
-
-
1
def _reduce_69(val, _values, result)
-
result = Node.new(:NOT, [val[1]])
-
-
result
-
end
-
-
# reduce 70 omitted
-
-
# reduce 71 omitted
-
-
# reduce 72 omitted
-
-
# reduce 73 omitted
-
-
# reduce 74 omitted
-
-
1
def _reduce_none(val, _values, result)
-
val[0]
-
end
-
-
end # class Parser
-
end # module CSS
-
end # module Nokogiri
-
1
require 'thread'
-
-
1
module Nokogiri
-
1
module CSS
-
1
class Parser < Racc::Parser
-
1
@cache_on = true
-
1
@cache = {}
-
1
@mutex = Mutex.new
-
-
1
class << self
-
# Turn on CSS parse caching
-
1
attr_accessor :cache_on
-
1
alias :cache_on? :cache_on
-
1
alias :set_cache :cache_on=
-
-
# Get the css selector in +string+ from the cache
-
1
def [] string
-
return unless @cache_on
-
@mutex.synchronize { @cache[string] }
-
end
-
-
# Set the css selector in +string+ in the cache to +value+
-
1
def []= string, value
-
return value unless @cache_on
-
@mutex.synchronize { @cache[string] = value }
-
end
-
-
# Clear the cache
-
1
def clear_cache
-
@mutex.synchronize { @cache = {} }
-
end
-
-
# Execute +block+ without cache
-
1
def without_cache &block
-
tmp = @cache_on
-
@cache_on = false
-
block.call
-
@cache_on = tmp
-
end
-
-
###
-
# Parse this CSS selector in +selector+. Returns an AST.
-
1
def parse selector
-
@warned ||= false
-
unless @warned
-
$stderr.puts('Nokogiri::CSS::Parser.parse is deprecated, call Nokogiri::CSS.parse(), this will be removed August 1st or version 1.4.0 (whichever is first)')
-
@warned = true
-
end
-
new.parse selector
-
end
-
end
-
-
# Create a new CSS parser with respect to +namespaces+
-
1
def initialize namespaces = {}
-
@tokenizer = Tokenizer.new
-
@namespaces = namespaces
-
super()
-
end
-
-
1
def parse string
-
@tokenizer.scan_setup string
-
do_parse
-
end
-
-
1
def next_token
-
@tokenizer.next_token
-
end
-
-
# Get the xpath for +string+ using +options+
-
1
def xpath_for string, options={}
-
key = "#{string}#{options[:ns]}#{options[:prefix]}"
-
v = self.class[key]
-
return v if v
-
-
args = [
-
options[:prefix] || '//',
-
options[:visitor] || XPathVisitor.new
-
]
-
self.class[key] = parse(string).map { |ast|
-
ast.to_xpath(*args)
-
}
-
end
-
-
# On CSS parser error, raise an exception
-
1
def on_error error_token_id, error_value, value_stack
-
after = value_stack.compact.last
-
raise SyntaxError.new("unexpected '#{error_value}' after '#{after}'")
-
end
-
end
-
end
-
end
-
1
require 'nokogiri/syntax_error'
-
1
module Nokogiri
-
1
module CSS
-
1
class SyntaxError < ::Nokogiri::SyntaxError
-
end
-
end
-
end
-
#--
-
# DO NOT MODIFY!!!!
-
# This file is automatically generated by rex 1.0.5
-
# from lexical definition file "lib/nokogiri/css/tokenizer.rex".
-
#++
-
-
1
module Nokogiri
-
1
module CSS
-
1
class Tokenizer # :nodoc:
-
1
require 'strscan'
-
-
1
class ScanError < StandardError ; end
-
-
1
attr_reader :lineno
-
1
attr_reader :filename
-
1
attr_accessor :state
-
-
1
def scan_setup(str)
-
@ss = StringScanner.new(str)
-
@lineno = 1
-
@state = nil
-
end
-
-
1
def action
-
yield
-
end
-
-
1
def scan_str(str)
-
scan_setup(str)
-
do_parse
-
end
-
1
alias :scan :scan_str
-
-
1
def load_file( filename )
-
@filename = filename
-
open(filename, "r") do |f|
-
scan_setup(f.read)
-
end
-
end
-
-
1
def scan_file( filename )
-
load_file(filename)
-
do_parse
-
end
-
-
-
1
def next_token
-
return if @ss.eos?
-
-
# skips empty actions
-
until token = _next_token or @ss.eos?; end
-
token
-
end
-
-
1
def _next_token
-
text = @ss.peek(1)
-
@lineno += 1 if text == "\n"
-
token = case @state
-
when nil
-
case
-
when (text = @ss.scan(/has\([\s]*/))
-
action { [:HAS, text] }
-
-
when (text = @ss.scan(/[-@]?([_A-Za-z]|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])([_A-Za-z0-9-]|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*\([\s]*/))
-
action { [:FUNCTION, text] }
-
-
when (text = @ss.scan(/[-@]?([_A-Za-z]|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])([_A-Za-z0-9-]|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*/))
-
action { [:IDENT, text] }
-
-
when (text = @ss.scan(/\#([_A-Za-z0-9-]|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])+/))
-
action { [:HASH, text] }
-
-
when (text = @ss.scan(/[\s]*~=[\s]*/))
-
action { [:INCLUDES, text] }
-
-
when (text = @ss.scan(/[\s]*\|=[\s]*/))
-
action { [:DASHMATCH, text] }
-
-
when (text = @ss.scan(/[\s]*\^=[\s]*/))
-
action { [:PREFIXMATCH, text] }
-
-
when (text = @ss.scan(/[\s]*\$=[\s]*/))
-
action { [:SUFFIXMATCH, text] }
-
-
when (text = @ss.scan(/[\s]*\*=[\s]*/))
-
action { [:SUBSTRINGMATCH, text] }
-
-
when (text = @ss.scan(/[\s]*!=[\s]*/))
-
action { [:NOT_EQUAL, text] }
-
-
when (text = @ss.scan(/[\s]*=[\s]*/))
-
action { [:EQUAL, text] }
-
-
when (text = @ss.scan(/[\s]*\)/))
-
action { [:RPAREN, text] }
-
-
when (text = @ss.scan(/\[[\s]*/))
-
action { [:LSQUARE, text] }
-
-
when (text = @ss.scan(/[\s]*\]/))
-
action { [:RSQUARE, text] }
-
-
when (text = @ss.scan(/[\s]*\+[\s]*/))
-
action { [:PLUS, text] }
-
-
when (text = @ss.scan(/[\s]*>[\s]*/))
-
action { [:GREATER, text] }
-
-
when (text = @ss.scan(/[\s]*,[\s]*/))
-
action { [:COMMA, text] }
-
-
when (text = @ss.scan(/[\s]*~[\s]*/))
-
action { [:TILDE, text] }
-
-
when (text = @ss.scan(/\:not\([\s]*/))
-
action { [:NOT, text] }
-
-
when (text = @ss.scan(/-?([0-9]+|[0-9]*\.[0-9]+)/))
-
action { [:NUMBER, text] }
-
-
when (text = @ss.scan(/[\s]*\/\/[\s]*/))
-
action { [:DOUBLESLASH, text] }
-
-
when (text = @ss.scan(/[\s]*\/[\s]*/))
-
action { [:SLASH, text] }
-
-
when (text = @ss.scan(/U\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?/))
-
action {[:UNICODE_RANGE, text] }
-
-
when (text = @ss.scan(/[\s]+/))
-
action { [:S, text] }
-
-
when (text = @ss.scan(/"([^\n\r\f"]|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*"|'([^\n\r\f']|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*'/))
-
action { [:STRING, text] }
-
-
when (text = @ss.scan(/./))
-
action { [text, text] }
-
-
else
-
text = @ss.string[@ss.pos .. -1]
-
raise ScanError, "can not match: '" + text + "'"
-
end # if
-
-
else
-
raise ScanError, "undefined state: '" + state.to_s + "'"
-
end # case state
-
token
-
end # def _next_token
-
-
end # class
-
end
-
end
-
1
module Nokogiri
-
1
module CSS
-
1
class XPathVisitor # :nodoc:
-
1
def visit_function node
-
-
msg = :"visit_function_#{node.value.first.gsub(/[(]/, '')}"
-
return self.send(msg, node) if self.respond_to?(msg)
-
-
case node.value.first
-
when /^text\(/
-
'child::text()'
-
when /^self\(/
-
"self::#{node.value[1]}"
-
when /^eq\(/
-
"position() = #{node.value[1]}"
-
when /^(nth|nth-of-type)\(/
-
if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
-
nth(node.value[1])
-
else
-
"position() = #{node.value[1]}"
-
end
-
when /^nth-child\(/
-
if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
-
nth(node.value[1], :child => true)
-
else
-
"count(preceding-sibling::*) = #{node.value[1].to_i-1}"
-
end
-
when /^nth-last-of-type\(/
-
if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
-
nth(node.value[1], :last => true)
-
else
-
index = node.value[1].to_i - 1
-
index == 0 ? "position() = last()" : "position() = last() - #{index}"
-
end
-
when /^nth-last-child\(/
-
if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
-
nth(node.value[1], :last => true, :child => true)
-
else
-
"count(following-sibling::*) = #{node.value[1].to_i-1}"
-
end
-
when /^(first|first-of-type)\(/
-
"position() = 1"
-
when /^(last|last-of-type)\(/
-
"position() = last()"
-
when /^contains\(/
-
"contains(., #{node.value[1]})"
-
when /^gt\(/
-
"position() > #{node.value[1]}"
-
when /^only-child\(/
-
"last() = 1"
-
when /^comment\(/
-
"comment()"
-
when /^has\(/
-
node.value[1].accept(self)
-
else
-
args = ['.'] + node.value[1..-1]
-
"#{node.value.first}#{args.join(', ')})"
-
end
-
end
-
-
1
def visit_not node
-
child = node.value.first
-
if :ELEMENT_NAME == child.type
-
"not(self::#{child.accept(self)})"
-
else
-
"not(#{child.accept(self)})"
-
end
-
end
-
-
1
def visit_id node
-
node.value.first =~ /^#(.*)$/
-
"@id = '#{$1}'"
-
end
-
-
1
def visit_attribute_condition node
-
attribute = if (node.value.first.type == :FUNCTION) or (node.value.first.value.first =~ /::/)
-
''
-
else
-
'@'
-
end
-
attribute += node.value.first.accept(self)
-
-
# Support non-standard css
-
attribute.gsub!(/^@@/, '@')
-
-
return attribute unless node.value.length == 3
-
-
value = node.value.last
-
value = "'#{value}'" if value !~ /^['"]/
-
-
case node.value[1]
-
when :equal
-
attribute + " = " + "#{value}"
-
when :not_equal
-
attribute + " != " + "#{value}"
-
when :substring_match
-
"contains(#{attribute}, #{value})"
-
when :prefix_match
-
"starts-with(#{attribute}, #{value})"
-
when :dash_match
-
"#{attribute} = #{value} or starts-with(#{attribute}, concat(#{value}, '-'))"
-
when :includes
-
"contains(concat(\" \", #{attribute}, \" \"),concat(\" \", #{value}, \" \"))"
-
when :suffix_match
-
"substring(#{attribute}, string-length(#{attribute}) - " +
-
"string-length(#{value}) + 1, string-length(#{value})) = #{value}"
-
else
-
attribute + " #{node.value[1]} " + "#{value}"
-
end
-
end
-
-
1
def visit_pseudo_class node
-
if node.value.first.is_a?(Nokogiri::CSS::Node) and node.value.first.type == :FUNCTION
-
node.value.first.accept(self)
-
else
-
msg = :"visit_pseudo_class_#{node.value.first.gsub(/[(]/, '')}"
-
return self.send(msg, node) if self.respond_to?(msg)
-
-
case node.value.first
-
when "first" then "position() = 1"
-
when "first-child" then "count(preceding-sibling::*) = 0"
-
when "last" then "position() = last()"
-
when "last-child" then "count(following-sibling::*) = 0"
-
when "first-of-type" then "position() = 1"
-
when "last-of-type" then "position() = last()"
-
when "only-child" then "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0"
-
when "only-of-type" then "last() = 1"
-
when "empty" then "not(node())"
-
when "parent" then "node()"
-
when "root" then "not(parent::*)"
-
else
-
node.value.first + "(.)"
-
end
-
end
-
end
-
-
1
def visit_class_condition node
-
"contains(concat(' ', normalize-space(@class), ' '), ' #{node.value.first} ')"
-
end
-
-
1
def visit_combinator node
-
if is_of_type_pseudo_class?(node.value.last)
-
"#{node.value.first.accept(self) if node.value.first}][#{node.value.last.accept(self)}"
-
else
-
"#{node.value.first.accept(self) if node.value.first} and #{node.value.last.accept(self)}"
-
end
-
end
-
-
{
-
'direct_adjacent_selector' => "/following-sibling::*[1]/self::",
-
'following_selector' => "/following-sibling::",
-
'descendant_selector' => '//',
-
'child_selector' => '/',
-
1
}.each do |k,v|
-
4
class_eval %{
-
def visit_#{k} node
-
"\#{node.value.first.accept(self) if node.value.first}#{v}\#{node.value.last.accept(self)}"
-
end
-
}
-
end
-
-
1
def visit_conditional_selector node
-
node.value.first.accept(self) + '[' +
-
node.value.last.accept(self) + ']'
-
end
-
-
1
def visit_element_name node
-
node.value.first
-
end
-
-
1
def accept node
-
node.accept(self)
-
end
-
-
1
private
-
1
def nth node, options={}
-
raise ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}" unless node.value.size == 4
-
-
a, b = read_a_and_positive_b node.value
-
position = if options[:child]
-
options[:last] ? "(count(following-sibling::*) + 1)" : "(count(preceding-sibling::*) + 1)"
-
else
-
options[:last] ? "(last()-position()+1)" : "position()"
-
end
-
-
if b.zero?
-
"(#{position} mod #{a}) = 0"
-
else
-
compare = a < 0 ? "<=" : ">="
-
if a.abs == 1
-
"#{position} #{compare} #{b}"
-
else
-
"(#{position} #{compare} #{b}) and (((#{position}-#{b}) mod #{a.abs}) = 0)"
-
end
-
end
-
end
-
-
1
def read_a_and_positive_b values
-
op = values[2]
-
if op == "+"
-
a = values[0].to_i
-
b = values[3].to_i
-
elsif op == "-"
-
a = values[0].to_i
-
b = a - (values[3].to_i % a)
-
else
-
raise ArgumentError, "expected an+b node to have either + or - as the operator, but is #{op.inspect}"
-
end
-
[a, b]
-
end
-
-
1
def is_of_type_pseudo_class? node
-
if node.type==:PSEUDO_CLASS
-
if node.value[0].is_a?(Nokogiri::CSS::Node) and node.value[0].type == :FUNCTION
-
node.value[0].value[0]
-
else
-
node.value[0]
-
end =~ /(nth|first|last|only)-of-type(\()?/
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module Decorators
-
###
-
# The Slop decorator implements method missing such that a methods may be
-
# used instead of XPath or CSS. See Nokogiri.Slop
-
1
module Slop
-
# The default XPath search context for Slop
-
1
XPATH_PREFIX = "./"
-
-
###
-
# look for node with +name+. See Nokogiri.Slop
-
1
def method_missing name, *args, &block
-
if args.empty?
-
list = xpath("#{XPATH_PREFIX}#{name.to_s.sub(/^_/, '')}")
-
elsif args.first.is_a? Hash
-
hash = args.first
-
if hash[:css]
-
list = css("#{name}#{hash[:css]}")
-
elsif hash[:xpath]
-
conds = Array(hash[:xpath]).join(' and ')
-
list = xpath("#{XPATH_PREFIX}#{name}[#{conds}]")
-
end
-
else
-
CSS::Parser.without_cache do
-
list = xpath(
-
*CSS.xpath_for("#{name}#{args.first}", :prefix => XPATH_PREFIX)
-
)
-
end
-
end
-
-
super if list.empty?
-
list.length == 1 ? list.first : list
-
end
-
-
1
def respond_to_missing? name, include_private = false
-
list = xpath("#{XPATH_PREFIX}#{name.to_s.sub(/^_/, '')}")
-
-
!list.empty?
-
end
-
end
-
end
-
end
-
1
require 'nokogiri/html/entity_lookup'
-
1
require 'nokogiri/html/document'
-
1
require 'nokogiri/html/document_fragment'
-
1
require 'nokogiri/html/sax/parser_context'
-
1
require 'nokogiri/html/sax/parser'
-
1
require 'nokogiri/html/sax/push_parser'
-
1
require 'nokogiri/html/element_description'
-
1
require 'nokogiri/html/element_description_defaults'
-
-
1
module Nokogiri
-
1
class << self
-
###
-
# Parse HTML. Convenience method for Nokogiri::HTML::Document.parse
-
1
def HTML thing, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_HTML, &block
-
Nokogiri::HTML::Document.parse(thing, url, encoding, options, &block)
-
end
-
end
-
-
1
module HTML
-
1
class << self
-
###
-
# Parse HTML. Convenience method for Nokogiri::HTML::Document.parse
-
1
def parse thing, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_HTML, &block
-
Document.parse(thing, url, encoding, options, &block)
-
end
-
-
####
-
# Parse a fragment from +string+ in to a NodeSet.
-
1
def fragment string, encoding = nil
-
HTML::DocumentFragment.parse string, encoding
-
end
-
end
-
-
# Instance of Nokogiri::HTML::EntityLookup
-
1
NamedCharacters = EntityLookup.new
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
###
-
# Nokogiri HTML builder is used for building HTML documents. It is very
-
# similar to the Nokogiri::XML::Builder. In fact, you should go read the
-
# documentation for Nokogiri::XML::Builder before reading this
-
# documentation.
-
#
-
# == Synopsis:
-
#
-
# Create an HTML document with a body that has an onload attribute, and a
-
# span tag with a class of "bold" that has content of "Hello world".
-
#
-
# builder = Nokogiri::HTML::Builder.new do |doc|
-
# doc.html {
-
# doc.body(:onload => 'some_func();') {
-
# doc.span.bold {
-
# doc.text "Hello world"
-
# }
-
# }
-
# }
-
# end
-
# puts builder.to_html
-
#
-
# The HTML builder inherits from the XML builder, so make sure to read the
-
# Nokogiri::XML::Builder documentation.
-
1
class Builder < Nokogiri::XML::Builder
-
###
-
# Convert the builder to HTML
-
1
def to_html
-
@doc.to_html
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
class Document < Nokogiri::XML::Document
-
###
-
# Get the meta tag encoding for this document. If there is no meta tag,
-
# then nil is returned.
-
1
def meta_encoding
-
case
-
when meta = at('//meta[@charset]')
-
meta[:charset]
-
when meta = meta_content_type
-
meta['content'][/charset\s*=\s*([\w-]+)/i, 1]
-
end
-
end
-
-
###
-
# Set the meta tag encoding for this document.
-
#
-
# If an meta encoding tag is already present, its content is
-
# replaced with the given text.
-
#
-
# Otherwise, this method tries to create one at an appropriate
-
# place supplying head and/or html elements as necessary, which
-
# is inside a head element if any, and before any text node or
-
# content element (typically <body>) if any.
-
#
-
# The result when trying to set an encoding that is different
-
# from the document encoding is undefined.
-
#
-
# Beware in CRuby, that libxml2 automatically inserts a meta tag
-
# into a head element.
-
1
def meta_encoding= encoding
-
case
-
when meta = meta_content_type
-
meta['content'] = 'text/html; charset=%s' % encoding
-
encoding
-
when meta = at('//meta[@charset]')
-
meta['charset'] = encoding
-
else
-
meta = XML::Node.new('meta', self)
-
if dtd = internal_subset and dtd.html5_dtd?
-
meta['charset'] = encoding
-
else
-
meta['http-equiv'] = 'Content-Type'
-
meta['content'] = 'text/html; charset=%s' % encoding
-
end
-
-
case
-
when head = at('//head')
-
head.prepend_child(meta)
-
else
-
set_metadata_element(meta)
-
end
-
encoding
-
end
-
end
-
-
1
def meta_content_type
-
xpath('//meta[@http-equiv and boolean(@content)]').find { |node|
-
node['http-equiv'] =~ /\AContent-Type\z/i
-
}
-
end
-
1
private :meta_content_type
-
-
###
-
# Get the title string of this document. Return nil if there is
-
# no title tag.
-
1
def title
-
title = at('//title') and title.inner_text
-
end
-
-
###
-
# Set the title string of this document.
-
#
-
# If a title element is already present, its content is replaced
-
# with the given text.
-
#
-
# Otherwise, this method tries to create one at an appropriate
-
# place supplying head and/or html elements as necessary, which
-
# is inside a head element if any, right after a meta
-
# encoding/charset tag if any, and before any text node or
-
# content element (typically <body>) if any.
-
1
def title=(text)
-
tnode = XML::Text.new(text, self)
-
if title = at('//title')
-
title.children = tnode
-
return text
-
end
-
-
title = XML::Node.new('title', self) << tnode
-
case
-
when head = at('//head')
-
head << title
-
when meta = at('//meta[@charset]') || meta_content_type
-
# better put after charset declaration
-
meta.add_next_sibling(title)
-
else
-
set_metadata_element(title)
-
end
-
text
-
end
-
-
1
def set_metadata_element(element)
-
case
-
when head = at('//head')
-
head << element
-
when html = at('//html')
-
head = html.prepend_child(XML::Node.new('head', self))
-
head.prepend_child(element)
-
when first = children.find { |node|
-
case node
-
when XML::Element, XML::Text
-
true
-
end
-
}
-
# We reach here only if the underlying document model
-
# allows <html>/<head> elements to be omitted and does not
-
# automatically supply them.
-
first.add_previous_sibling(element)
-
else
-
html = add_child(XML::Node.new('html', self))
-
head = html.add_child(XML::Node.new('head', self))
-
head.prepend_child(element)
-
end
-
end
-
1
private :set_metadata_element
-
-
####
-
# Serialize Node using +options+. Save options can also be set using a
-
# block. See SaveOptions.
-
#
-
# These two statements are equivalent:
-
#
-
# node.serialize(:encoding => 'UTF-8', :save_with => FORMAT | AS_XML)
-
#
-
# or
-
#
-
# node.serialize(:encoding => 'UTF-8') do |config|
-
# config.format.as_xml
-
# end
-
#
-
1
def serialize options = {}
-
options[:save_with] ||= XML::Node::SaveOptions::DEFAULT_HTML
-
super
-
end
-
-
####
-
# Create a Nokogiri::XML::DocumentFragment from +tags+
-
1
def fragment tags = nil
-
DocumentFragment.new(self, tags, self.root)
-
end
-
-
1
class << self
-
###
-
# Parse HTML. +string_or_io+ may be a String, or any object that
-
# responds to _read_ and _close_ such as an IO, or StringIO.
-
# +url+ is resource where this document is located. +encoding+ is the
-
# encoding that should be used when processing the document. +options+
-
# is a number that sets options in the parser, such as
-
# Nokogiri::XML::ParseOptions::RECOVER. See the constants in
-
# Nokogiri::XML::ParseOptions.
-
1
def parse string_or_io, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_HTML
-
-
options = Nokogiri::XML::ParseOptions.new(options) if Integer === options
-
# Give the options to the user
-
yield options if block_given?
-
-
if string_or_io.respond_to?(:encoding)
-
unless string_or_io.encoding.name == "ASCII-8BIT"
-
encoding ||= string_or_io.encoding.name
-
end
-
end
-
-
if string_or_io.respond_to?(:read)
-
url ||= string_or_io.respond_to?(:path) ? string_or_io.path : nil
-
unless encoding
-
# Libxml2's parser has poor support for encoding
-
# detection. First, it does not recognize the HTML5
-
# style meta charset declaration. Secondly, even if it
-
# successfully detects an encoding hint, it does not
-
# re-decode or re-parse the preceding part which may be
-
# garbled.
-
#
-
# EncodingReader aims to perform advanced encoding
-
# detection beyond what Libxml2 does, and to emulate
-
# rewinding of a stream and make Libxml2 redo parsing
-
# from the start when an encoding hint is found.
-
string_or_io = EncodingReader.new(string_or_io)
-
begin
-
return read_io(string_or_io, url, encoding, options.to_i)
-
rescue EncodingFound => e
-
encoding = e.found_encoding
-
end
-
end
-
return read_io(string_or_io, url, encoding, options.to_i)
-
end
-
-
# read_memory pukes on empty docs
-
if string_or_io.nil? or string_or_io.empty?
-
return encoding ? new.tap { |i| i.encoding = encoding } : new
-
end
-
-
encoding ||= EncodingReader.detect_encoding(string_or_io)
-
-
read_memory(string_or_io, url, encoding, options.to_i)
-
end
-
end
-
-
1
class EncodingFound < StandardError # :nodoc:
-
1
attr_reader :found_encoding
-
-
1
def initialize(encoding)
-
@found_encoding = encoding
-
super("encoding found: %s" % encoding)
-
end
-
end
-
-
1
class EncodingReader # :nodoc:
-
1
class SAXHandler < Nokogiri::XML::SAX::Document # :nodoc:
-
1
attr_reader :encoding
-
-
1
def initialize
-
@encoding = nil
-
super()
-
end
-
-
1
def start_element(name, attrs = [])
-
return unless name == 'meta'
-
attr = Hash[attrs]
-
charset = attr['charset'] and
-
@encoding = charset
-
http_equiv = attr['http-equiv'] and
-
http_equiv.match(/\AContent-Type\z/i) and
-
content = attr['content'] and
-
m = content.match(/;\s*charset\s*=\s*([\w-]+)/) and
-
@encoding = m[1]
-
end
-
end
-
-
1
class JumpSAXHandler < SAXHandler
-
1
def initialize(jumptag)
-
@jumptag = jumptag
-
super()
-
end
-
-
1
def start_element(name, attrs = [])
-
super
-
throw @jumptag, @encoding if @encoding
-
throw @jumptag, nil if name =~ /\A(?:div|h1|img|p|br)\z/
-
end
-
end
-
-
1
def self.detect_encoding(chunk)
-
if Nokogiri.jruby? && EncodingReader.is_jruby_without_fix?
-
return EncodingReader.detect_encoding_for_jruby_without_fix(chunk)
-
end
-
m = chunk.match(/\A(<\?xml[ \t\r\n]+[^>]*>)/) and
-
return Nokogiri.XML(m[1]).encoding
-
-
if Nokogiri.jruby?
-
m = chunk.match(/(<meta\s)(.*)(charset\s*=\s*([\w-]+))(.*)/i) and
-
return m[4]
-
catch(:encoding_found) {
-
Nokogiri::HTML::SAX::Parser.new(JumpSAXHandler.new(:encoding_found)).parse(chunk)
-
nil
-
}
-
else
-
handler = SAXHandler.new
-
parser = Nokogiri::HTML::SAX::PushParser.new(handler)
-
parser << chunk rescue Nokogiri::SyntaxError
-
handler.encoding
-
end
-
end
-
-
1
def self.is_jruby_without_fix?
-
JRUBY_VERSION.split('.').join.to_i < 165
-
end
-
-
1
def self.detect_encoding_for_jruby_without_fix(chunk)
-
m = chunk.match(/\A(<\?xml[ \t\r\n]+[^>]*>)/) and
-
return Nokogiri.XML(m[1]).encoding
-
-
m = chunk.match(/(<meta\s)(.*)(charset\s*=\s*([\w-]+))(.*)/i) and
-
return m[4]
-
-
catch(:encoding_found) {
-
Nokogiri::HTML::SAX::Parser.new(JumpSAXHandler.new(:encoding_found.to_s)).parse(chunk)
-
nil
-
}
-
rescue Nokogiri::SyntaxError, RuntimeError
-
# Ignore parser errors that nokogiri may raise
-
nil
-
end
-
-
1
def initialize(io)
-
@io = io
-
@firstchunk = nil
-
@encoding_found = nil
-
end
-
-
# This method is used by the C extension so that
-
# Nokogiri::HTML::Document#read_io() does not leak memory when
-
# EncodingFound is raised.
-
1
attr_reader :encoding_found
-
-
1
def read(len)
-
# no support for a call without len
-
-
if !@firstchunk
-
@firstchunk = @io.read(len) or return nil
-
-
# This implementation expects that the first call from
-
# htmlReadIO() is made with a length long enough (~1KB) to
-
# achieve advanced encoding detection.
-
if encoding = EncodingReader.detect_encoding(@firstchunk)
-
# The first chunk is stored for the next read in retry.
-
raise @encoding_found = EncodingFound.new(encoding)
-
end
-
end
-
@encoding_found = nil
-
-
ret = @firstchunk.slice!(0, len)
-
if (len -= ret.length) > 0
-
rest = @io.read(len) and ret << rest
-
end
-
if ret.empty?
-
nil
-
else
-
ret
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
class DocumentFragment < Nokogiri::XML::DocumentFragment
-
####
-
# Create a Nokogiri::XML::DocumentFragment from +tags+, using +encoding+
-
1
def self.parse tags, encoding = nil
-
doc = HTML::Document.new
-
-
encoding ||= tags.respond_to?(:encoding) ? tags.encoding.name : 'UTF-8'
-
doc.encoding = encoding
-
-
new(doc, tags)
-
end
-
-
1
def initialize document, tags = nil, ctx = nil
-
return self unless tags
-
-
if ctx
-
preexisting_errors = document.errors.dup
-
node_set = ctx.parse("<div>#{tags}</div>")
-
node_set.first.children.each { |child| child.parent = self } unless node_set.empty?
-
self.errors = document.errors - preexisting_errors
-
else
-
# This is a horrible hack, but I don't care
-
if tags.strip =~ /^<body/i
-
path = "/html/body"
-
else
-
path = "/html/body/node()"
-
end
-
-
temp_doc = HTML::Document.parse "<html><body>#{tags}", nil, document.encoding
-
temp_doc.xpath(path).each { |child| child.parent = self }
-
self.errors = temp_doc.errors
-
end
-
children
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
class ElementDescription
-
###
-
# Is this element a block element?
-
1
def block?
-
!inline?
-
end
-
-
###
-
# Convert this description to a string
-
1
def to_s
-
"#{name}: #{description}"
-
end
-
-
###
-
# Inspection information
-
1
def inspect
-
"#<#{self.class.name}: #{name} #{description}>"
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
class ElementDescription
-
-
# Methods are defined protected by method_defined? because at
-
# this point the C-library or Java library is already loaded,
-
# and we don't want to clobber any methods that have been
-
# defined there.
-
-
1
Desc = Struct.new("HTMLElementDescription", :name,
-
:startTag, :endTag, :saveEndTag,
-
:empty, :depr, :dtd, :isinline,
-
:desc,
-
:subelts, :defaultsubelt,
-
:attrs_opt, :attrs_depr, :attrs_req)
-
-
# This is filled in down below.
-
1
DefaultDescriptions = Hash.new()
-
-
1
def default_desc
-
DefaultDescriptions[name.downcase]
-
end
-
1
private :default_desc
-
-
1
unless method_defined? :implied_start_tag?
-
def implied_start_tag?
-
d = default_desc
-
d ? d.startTag : nil
-
end
-
end
-
-
1
unless method_defined? :implied_end_tag?
-
def implied_end_tag?
-
d = default_desc
-
d ? d.endTag : nil
-
end
-
end
-
-
1
unless method_defined? :save_end_tag?
-
def save_end_tag?
-
d = default_desc
-
d ? d.saveEndTag : nil
-
end
-
end
-
-
1
unless method_defined? :deprecated?
-
def deprecated?
-
d = default_desc
-
d ? d.depr : nil
-
end
-
end
-
-
1
unless method_defined? :description
-
def description
-
d = default_desc
-
d ? d.desc : nil
-
end
-
end
-
-
1
unless method_defined? :default_sub_element
-
def default_sub_element
-
d = default_desc
-
d ? d.defaultsubelt : nil
-
end
-
end
-
-
1
unless method_defined? :optional_attributes
-
def optional_attributes
-
d = default_desc
-
d ? d.attrs_opt : []
-
end
-
end
-
-
1
unless method_defined? :deprecated_attributes
-
def deprecated_attributes
-
d = default_desc
-
d ? d.attrs_depr : []
-
end
-
end
-
-
1
unless method_defined? :required_attributes
-
def required_attributes
-
d = default_desc
-
d ? d.attrs_req : []
-
end
-
end
-
-
###
-
# Default Element Descriptions (HTML 4.0) copied from
-
# libxml2/HTMLparser.c and libxml2/include/libxml/HTMLparser.h
-
#
-
# The copyright notice for those files and the following list of
-
# element and attribute descriptions is reproduced here:
-
#
-
# Except where otherwise noted in the source code (e.g. the
-
# files hash.c, list.c and the trio files, which are covered by
-
# a similar licence but with different Copyright notices) all
-
# the files are:
-
#
-
# Copyright (C) 1998-2003 Daniel Veillard. All Rights Reserved.
-
#
-
# Permission is hereby granted, free of charge, to any person
-
# obtaining a copy of this software and associated documentation
-
# files (the "Software"), to deal in the Software without
-
# restriction, including without limitation the rights to use,
-
# copy, modify, merge, publish, distribute, sublicense, and/or
-
# sell copies of the Software, and to permit persons to whom the
-
# Software is fur- nished to do so, subject to the following
-
# conditions:
-
-
# The above copyright notice and this permission notice shall be
-
# included in all copies or substantial portions of the
-
# Software.
-
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-
# WARRANTIES OF MERCHANTABILITY, FIT- NESS FOR A PARTICULAR
-
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE DANIEL
-
# VEILLARD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-
# FROM, OUT OF OR IN CON- NECTION WITH THE SOFTWARE OR THE USE
-
# OR OTHER DEALINGS IN THE SOFTWARE.
-
-
# Except as contained in this notice, the name of Daniel
-
# Veillard shall not be used in advertising or otherwise to
-
# promote the sale, use or other deal- ings in this Software
-
# without prior written authorization from him.
-
-
# Attributes defined and categorized
-
1
FONTSTYLE = ["tt", "i", "b", "u", "s", "strike", "big", "small"]
-
1
PHRASE = ['em', 'strong', 'dfn', 'code', 'samp',
-
'kbd', 'var', 'cite', 'abbr', 'acronym']
-
1
SPECIAL = ['a', 'img', 'applet', 'embed', 'object', 'font','basefont',
-
'br', 'script', 'map', 'q', 'sub', 'sup', 'span', 'bdo',
-
'iframe']
-
1
PCDATA = []
-
1
HEADING = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
-
1
LIST = ['ul', 'ol', 'dir', 'menu']
-
1
FORMCTRL = ['input', 'select', 'textarea', 'label', 'button']
-
1
BLOCK = [HEADING, LIST, 'pre', 'p', 'dl', 'div', 'center', 'noscript',
-
'noframes', 'blockquote', 'form', 'isindex', 'hr', 'table',
-
'fieldset', 'address']
-
1
INLINE = [PCDATA, FONTSTYLE, PHRASE, SPECIAL, FORMCTRL]
-
1
FLOW = [BLOCK, INLINE]
-
1
MODIFIER = []
-
1
EMPTY = []
-
-
1
HTML_FLOW = FLOW
-
1
HTML_INLINE = INLINE
-
1
HTML_PCDATA = PCDATA
-
1
HTML_CDATA = HTML_PCDATA
-
-
1
COREATTRS = ['id', 'class', 'style', 'title']
-
1
I18N = ['lang', 'dir']
-
1
EVENTS = ['onclick', 'ondblclick', 'onmousedown', 'onmouseup',
-
'onmouseover', 'onmouseout', 'onkeypress', 'onkeydown',
-
'onkeyup']
-
1
ATTRS = [COREATTRS, I18N,EVENTS]
-
1
CELLHALIGN = ['align', 'char', 'charoff']
-
1
CELLVALIGN = ['valign']
-
-
1
HTML_ATTRS = ATTRS
-
1
CORE_I18N_ATTRS = [COREATTRS, I18N]
-
1
CORE_ATTRS = COREATTRS
-
1
I18N_ATTRS = I18N
-
-
-
1
A_ATTRS = [ATTRS, 'charset', 'type', 'name',
-
'href', 'hreflang', 'rel', 'rev', 'accesskey', 'shape',
-
'coords', 'tabindex', 'onfocus', 'onblur']
-
1
TARGET_ATTR = ['target']
-
1
ROWS_COLS_ATTR = ['rows', 'cols']
-
1
ALT_ATTR = ['alt']
-
1
SRC_ALT_ATTRS = ['src', 'alt']
-
1
HREF_ATTRS = ['href']
-
1
CLEAR_ATTRS = ['clear']
-
1
INLINE_P = [INLINE, 'p']
-
-
1
FLOW_PARAM = [FLOW, 'param']
-
1
APPLET_ATTRS = [COREATTRS , 'codebase',
-
'archive', 'alt', 'name', 'height', 'width', 'align',
-
'hspace', 'vspace']
-
1
AREA_ATTRS = ['shape', 'coords', 'href', 'nohref',
-
'tabindex', 'accesskey', 'onfocus', 'onblur']
-
1
BASEFONT_ATTRS = ['id', 'size', 'color', 'face']
-
1
QUOTE_ATTRS = [ATTRS, 'cite']
-
1
BODY_CONTENTS = [FLOW, 'ins', 'del']
-
1
BODY_ATTRS = [ATTRS, 'onload', 'onunload']
-
1
BODY_DEPR = ['background', 'bgcolor', 'text',
-
'link', 'vlink', 'alink']
-
1
BUTTON_ATTRS = [ATTRS, 'name', 'value', 'type',
-
'disabled', 'tabindex', 'accesskey', 'onfocus', 'onblur']
-
-
-
1
COL_ATTRS = [ATTRS, 'span', 'width', CELLHALIGN, CELLVALIGN]
-
1
COL_ELT = ['col']
-
1
EDIT_ATTRS = [ATTRS, 'datetime', 'cite']
-
1
COMPACT_ATTRS = [ATTRS, 'compact']
-
1
DL_CONTENTS = ['dt', 'dd']
-
1
COMPACT_ATTR = ['compact']
-
1
LABEL_ATTR = ['label']
-
1
FIELDSET_CONTENTS = [FLOW, 'legend' ]
-
1
FONT_ATTRS = [COREATTRS, I18N, 'size', 'color', 'face' ]
-
1
FORM_CONTENTS = [HEADING, LIST, INLINE, 'pre', 'p', 'div', 'center',
-
'noscript', 'noframes', 'blockquote', 'isindex', 'hr',
-
'table', 'fieldset', 'address']
-
1
FORM_ATTRS = [ATTRS, 'method', 'enctype', 'accept', 'name', 'onsubmit',
-
'onreset', 'accept-charset']
-
1
FRAME_ATTRS = [COREATTRS, 'longdesc', 'name', 'src', 'frameborder',
-
'marginwidth', 'marginheight', 'noresize', 'scrolling' ]
-
1
FRAMESET_ATTRS = [COREATTRS, 'rows', 'cols', 'onload', 'onunload']
-
1
FRAMESET_CONTENTS = ['frameset', 'frame', 'noframes']
-
1
HEAD_ATTRS = [I18N, 'profile']
-
1
HEAD_CONTENTS = ['title', 'isindex', 'base', 'script', 'style', 'meta',
-
'link', 'object']
-
1
HR_DEPR = ['align', 'noshade', 'size', 'width']
-
1
VERSION_ATTR = ['version']
-
1
HTML_CONTENT = ['head', 'body', 'frameset']
-
1
IFRAME_ATTRS = [COREATTRS, 'longdesc', 'name', 'src', 'frameborder',
-
'marginwidth', 'marginheight', 'scrolling', 'align',
-
'height', 'width']
-
1
IMG_ATTRS = [ATTRS, 'longdesc', 'name', 'height', 'width', 'usemap',
-
'ismap']
-
1
EMBED_ATTRS = [COREATTRS, 'align', 'alt', 'border', 'code', 'codebase',
-
'frameborder', 'height', 'hidden', 'hspace', 'name',
-
'palette', 'pluginspace', 'pluginurl', 'src', 'type',
-
'units', 'vspace', 'width']
-
1
INPUT_ATTRS = [ATTRS, 'type', 'name', 'value', 'checked', 'disabled',
-
'readonly', 'size', 'maxlength', 'src', 'alt', 'usemap',
-
'ismap', 'tabindex', 'accesskey', 'onfocus', 'onblur',
-
'onselect', 'onchange', 'accept']
-
1
PROMPT_ATTRS = [COREATTRS, I18N, 'prompt']
-
1
LABEL_ATTRS = [ATTRS, 'for', 'accesskey', 'onfocus', 'onblur']
-
1
LEGEND_ATTRS = [ATTRS, 'accesskey']
-
1
ALIGN_ATTR = ['align']
-
1
LINK_ATTRS = [ATTRS, 'charset', 'href', 'hreflang', 'type', 'rel', 'rev',
-
'media']
-
1
MAP_CONTENTS = [BLOCK, 'area']
-
1
NAME_ATTR = ['name']
-
1
ACTION_ATTR = ['action']
-
1
BLOCKLI_ELT = [BLOCK, 'li']
-
1
META_ATTRS = [I18N, 'http-equiv', 'name', 'scheme']
-
1
CONTENT_ATTR = ['content']
-
1
TYPE_ATTR = ['type']
-
1
NOFRAMES_CONTENT = ['body', FLOW, MODIFIER]
-
1
OBJECT_CONTENTS = [FLOW, 'param']
-
1
OBJECT_ATTRS = [ATTRS, 'declare', 'classid', 'codebase', 'data', 'type',
-
'codetype', 'archive', 'standby', 'height', 'width',
-
'usemap', 'name', 'tabindex']
-
1
OBJECT_DEPR = ['align', 'border', 'hspace', 'vspace']
-
1
OL_ATTRS = ['type', 'compact', 'start']
-
1
OPTION_ELT = ['option']
-
1
OPTGROUP_ATTRS = [ATTRS, 'disabled']
-
1
OPTION_ATTRS = [ATTRS, 'disabled', 'label', 'selected', 'value']
-
1
PARAM_ATTRS = ['id', 'value', 'valuetype', 'type']
-
1
WIDTH_ATTR = ['width']
-
1
PRE_CONTENT = [PHRASE, 'tt', 'i', 'b', 'u', 's', 'strike', 'a', 'br',
-
'script', 'map', 'q', 'span', 'bdo', 'iframe']
-
1
SCRIPT_ATTRS = ['charset', 'src', 'defer', 'event', 'for']
-
1
LANGUAGE_ATTR = ['language']
-
1
SELECT_CONTENT = ['optgroup', 'option']
-
1
SELECT_ATTRS = [ATTRS, 'name', 'size', 'multiple', 'disabled', 'tabindex',
-
'onfocus', 'onblur', 'onchange']
-
1
STYLE_ATTRS = [I18N, 'media', 'title']
-
1
TABLE_ATTRS = [ATTRS, 'summary', 'width', 'border', 'frame', 'rules',
-
'cellspacing', 'cellpadding', 'datapagesize']
-
1
TABLE_DEPR = ['align', 'bgcolor']
-
1
TABLE_CONTENTS = ['caption', 'col', 'colgroup', 'thead', 'tfoot', 'tbody',
-
'tr']
-
1
TR_ELT = ['tr']
-
1
TALIGN_ATTRS = [ATTRS, CELLHALIGN, CELLVALIGN]
-
1
TH_TD_DEPR = ['nowrap', 'bgcolor', 'width', 'height']
-
1
TH_TD_ATTR = [ATTRS, 'abbr', 'axis', 'headers', 'scope', 'rowspan',
-
'colspan', CELLHALIGN, CELLVALIGN]
-
1
TEXTAREA_ATTRS = [ATTRS, 'name', 'disabled', 'readonly', 'tabindex',
-
'accesskey', 'onfocus', 'onblur', 'onselect',
-
'onchange']
-
1
TR_CONTENTS = ['th', 'td']
-
1
BGCOLOR_ATTR = ['bgcolor']
-
1
LI_ELT = ['li']
-
1
UL_DEPR = ['type', 'compact']
-
1
DIR_ATTR = ['dir']
-
-
[
-
['a', false, false, false, false, false, :any, true,
-
'anchor ',
-
HTML_INLINE, nil, A_ATTRS, TARGET_ATTR, []
-
],
-
['abbr', false, false, false, false, false, :any, true,
-
'abbreviated form',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['acronym', false, false, false, false, false, :any, true, '',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['address', false, false, false, false, false, :any, false,
-
'information on author',
-
INLINE_P , nil, HTML_ATTRS, [], []
-
],
-
['applet', false, false, false, false, true, :loose, true,
-
'java applet ',
-
FLOW_PARAM, nil, [], APPLET_ATTRS, []
-
],
-
['area', false, true, true, true, false, :any, false,
-
'client-side image map area ',
-
EMPTY, nil, AREA_ATTRS, TARGET_ATTR, ALT_ATTR
-
],
-
['b', false, true, false, false, false, :any, true,
-
'bold text style',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['base', false, true, true, true, false, :any, false,
-
'document base uri ',
-
EMPTY, nil, [], TARGET_ATTR, HREF_ATTRS
-
],
-
['basefont', false, true, true, true, true, :loose, true,
-
'base font size ',
-
EMPTY, nil, [], BASEFONT_ATTRS, []
-
],
-
['bdo', false, false, false, false, false, :any, true,
-
'i18n bidi over-ride ',
-
HTML_INLINE, nil, CORE_I18N_ATTRS, [], DIR_ATTR
-
],
-
['big', false, true, false, false, false, :any, true,
-
'large text style',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['blockquote', false, false, false, false, false, :any, false,
-
'long quotation ',
-
HTML_FLOW, nil, QUOTE_ATTRS, [], []
-
],
-
['body', true, true, false, false, false, :any, false,
-
'document body ',
-
BODY_CONTENTS, 'div', BODY_ATTRS, BODY_DEPR, []
-
],
-
['br', false, true, true, true, false, :any, true,
-
'forced line break ',
-
EMPTY, nil, CORE_ATTRS, CLEAR_ATTRS, []
-
],
-
['button', false, false, false, false, false, :any, true,
-
'push button ',
-
[HTML_FLOW, MODIFIER], nil, BUTTON_ATTRS, [], []
-
],
-
['caption', false, false, false, false, false, :any, false,
-
'table caption ',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['center', false, true, false, false, true, :loose, false,
-
'shorthand for div align=center ',
-
HTML_FLOW, nil, [], HTML_ATTRS, []
-
],
-
['cite', false, false, false, false, false, :any, true, 'citation',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['code', false, false, false, false, false, :any, true,
-
'computer code fragment',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['col', false, true, true, true, false, :any, false, 'table column ',
-
EMPTY, nil, COL_ATTRS, [], []
-
],
-
['colgroup', false, true, false, false, false, :any, false,
-
'table column group ',
-
COL_ELT, 'col', COL_ATTRS, [], []
-
],
-
['dd', false, true, false, false, false, :any, false,
-
'definition description ',
-
HTML_FLOW, nil, HTML_ATTRS, [], []
-
],
-
['del', false, false, false, false, false, :any, true,
-
'deleted text ',
-
HTML_FLOW, nil, EDIT_ATTRS, [], []
-
],
-
['dfn', false, false, false, false, false, :any, true,
-
'instance definition',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['dir', false, false, false, false, true, :loose, false,
-
'directory list',
-
BLOCKLI_ELT, 'li', [], COMPACT_ATTRS, []
-
],
-
['div', false, false, false, false, false, :any, false,
-
'generic language/style container',
-
HTML_FLOW, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['dl', false, false, false, false, false, :any, false,
-
'definition list ',
-
DL_CONTENTS, 'dd', HTML_ATTRS, COMPACT_ATTR, []
-
],
-
['dt', false, true, false, false, false, :any, false,
-
'definition term ',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['em', false, true, false, false, false, :any, true,
-
'emphasis',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['embed', false, true, false, false, true, :loose, true,
-
'generic embedded object ',
-
EMPTY, nil, EMBED_ATTRS, [], []
-
],
-
['fieldset', false, false, false, false, false, :any, false,
-
'form control group ',
-
FIELDSET_CONTENTS, nil, HTML_ATTRS, [], []
-
],
-
['font', false, true, false, false, true, :loose, true,
-
'local change to font ',
-
HTML_INLINE, nil, [], FONT_ATTRS, []
-
],
-
['form', false, false, false, false, false, :any, false,
-
'interactive form ',
-
FORM_CONTENTS, 'fieldset', FORM_ATTRS, TARGET_ATTR, ACTION_ATTR
-
],
-
['frame', false, true, true, true, false, :frameset, false,
-
'subwindow ',
-
EMPTY, nil, [], FRAME_ATTRS, []
-
],
-
['frameset', false, false, false, false, false, :frameset, false,
-
'window subdivision',
-
FRAMESET_CONTENTS, 'noframes', [], FRAMESET_ATTRS, []
-
],
-
['htrue', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['htrue', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['htrue', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['h4', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['h5', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['h6', false, false, false, false, false, :any, false,
-
'heading ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['head', true, true, false, false, false, :any, false,
-
'document head ',
-
HEAD_CONTENTS, nil, HEAD_ATTRS, [], []
-
],
-
['hr', false, true, true, true, false, :any, false,
-
'horizontal rule ',
-
EMPTY, nil, HTML_ATTRS, HR_DEPR, []
-
],
-
['html', true, true, false, false, false, :any, false,
-
'document root element ',
-
HTML_CONTENT, nil, I18N_ATTRS, VERSION_ATTR, []
-
],
-
['i', false, true, false, false, false, :any, true,
-
'italic text style',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['iframe', false, false, false, false, false, :any, true,
-
'inline subwindow ',
-
HTML_FLOW, nil, [], IFRAME_ATTRS, []
-
],
-
['img', false, true, true, true, false, :any, true,
-
'embedded image ',
-
EMPTY, nil, IMG_ATTRS, ALIGN_ATTR, SRC_ALT_ATTRS
-
],
-
['input', false, true, true, true, false, :any, true,
-
'form control ',
-
EMPTY, nil, INPUT_ATTRS, ALIGN_ATTR, []
-
],
-
['ins', false, false, false, false, false, :any, true,
-
'inserted text',
-
HTML_FLOW, nil, EDIT_ATTRS, [], []
-
],
-
['isindex', false, true, true, true, true, :loose, false,
-
'single line prompt ',
-
EMPTY, nil, [], PROMPT_ATTRS, []
-
],
-
['kbd', false, false, false, false, false, :any, true,
-
'text to be entered by the user',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['label', false, false, false, false, false, :any, true,
-
'form field label text ',
-
[HTML_INLINE, MODIFIER], nil, LABEL_ATTRS, [], []
-
],
-
['legend', false, false, false, false, false, :any, false,
-
'fieldset legend ',
-
HTML_INLINE, nil, LEGEND_ATTRS, ALIGN_ATTR, []
-
],
-
['li', false, true, true, false, false, :any, false,
-
'list item ',
-
HTML_FLOW, nil, HTML_ATTRS, [], []
-
],
-
['link', false, true, true, true, false, :any, false,
-
'a media-independent link ',
-
EMPTY, nil, LINK_ATTRS, TARGET_ATTR, []
-
],
-
['map', false, false, false, false, false, :any, true,
-
'client-side image map ',
-
MAP_CONTENTS, nil, HTML_ATTRS, [], NAME_ATTR
-
],
-
['menu', false, false, false, false, true, :loose, false,
-
'menu list ',
-
BLOCKLI_ELT, nil, [], COMPACT_ATTRS, []
-
],
-
['meta', false, true, true, true, false, :any, false,
-
'generic metainformation ',
-
EMPTY, nil, META_ATTRS, [], CONTENT_ATTR
-
],
-
['noframes', false, false, false, false, false, :frameset, false,
-
'alternate content container for non frame-based rendering ',
-
NOFRAMES_CONTENT, 'body', HTML_ATTRS, [], []
-
],
-
['noscript', false, false, false, false, false, :any, false,
-
'alternate content container for non script-based rendering ',
-
HTML_FLOW, 'div', HTML_ATTRS, [], []
-
],
-
['object', false, false, false, false, false, :any, true,
-
'generic embedded object ',
-
OBJECT_CONTENTS, 'div', OBJECT_ATTRS, OBJECT_DEPR, []
-
],
-
['ol', false, false, false, false, false, :any, false,
-
'ordered list ',
-
LI_ELT, 'li', HTML_ATTRS, OL_ATTRS, []
-
],
-
['optgroup', false, false, false, false, false, :any, false,
-
'option group ',
-
OPTION_ELT, 'option', OPTGROUP_ATTRS, [], LABEL_ATTR
-
],
-
['option', false, true, false, false, false, :any, false,
-
'selectable choice ',
-
HTML_PCDATA, nil, OPTION_ATTRS, [], []
-
],
-
['p', false, true, false, false, false, :any, false,
-
'paragraph ',
-
HTML_INLINE, nil, HTML_ATTRS, ALIGN_ATTR, []
-
],
-
['param', false, true, true, true, false, :any, false,
-
'named property value ',
-
EMPTY, nil, PARAM_ATTRS, [], NAME_ATTR
-
],
-
['pre', false, false, false, false, false, :any, false,
-
'preformatted text ',
-
PRE_CONTENT, nil, HTML_ATTRS, WIDTH_ATTR, []
-
],
-
['q', false, false, false, false, false, :any, true,
-
'short inline quotation ',
-
HTML_INLINE, nil, QUOTE_ATTRS, [], []
-
],
-
['s', false, true, false, false, true, :loose, true,
-
'strike-through text style',
-
HTML_INLINE, nil, [], HTML_ATTRS, []
-
],
-
['samp', false, false, false, false, false, :any, true,
-
'sample program output, scripts, etc.',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['script', false, false, false, false, false, :any, true,
-
'script statements ',
-
HTML_CDATA, nil, SCRIPT_ATTRS, LANGUAGE_ATTR, TYPE_ATTR
-
],
-
['select', false, false, false, false, false, :any, true,
-
'option selector ',
-
SELECT_CONTENT, nil, SELECT_ATTRS, [], []
-
],
-
['small', false, true, false, false, false, :any, true,
-
'small text style',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['span', false, false, false, false, false, :any, true,
-
'generic language/style container ',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['strike', false, true, false, false, true, :loose, true,
-
'strike-through text',
-
HTML_INLINE, nil, [], HTML_ATTRS, []
-
],
-
['strong', false, true, false, false, false, :any, true,
-
'strong emphasis',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['style', false, false, false, false, false, :any, false,
-
'style info ',
-
HTML_CDATA, nil, STYLE_ATTRS, [], TYPE_ATTR
-
],
-
['sub', false, true, false, false, false, :any, true,
-
'subscript',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['sup', false, true, false, false, false, :any, true,
-
'superscript ',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['table', false, false, false, false, false, :any, false,
-
'',
-
TABLE_CONTENTS, 'tr', TABLE_ATTRS, TABLE_DEPR, []
-
],
-
['tbody', true, false, false, false, false, :any, false,
-
'table body ',
-
TR_ELT, 'tr', TALIGN_ATTRS, [], []
-
],
-
['td', false, false, false, false, false, :any, false,
-
'table data cell',
-
HTML_FLOW, nil, TH_TD_ATTR, TH_TD_DEPR, []
-
],
-
['textarea', false, false, false, false, false, :any, true,
-
'multi-line text field ',
-
HTML_PCDATA, nil, TEXTAREA_ATTRS, [], ROWS_COLS_ATTR
-
],
-
['tfoot', false, true, false, false, false, :any, false,
-
'table footer ',
-
TR_ELT, 'tr', TALIGN_ATTRS, [], []
-
],
-
['th', false, true, false, false, false, :any, false,
-
'table header cell',
-
HTML_FLOW, nil, TH_TD_ATTR, TH_TD_DEPR, []
-
],
-
['thead', false, true, false, false, false, :any, false,
-
'table header ',
-
TR_ELT, 'tr', TALIGN_ATTRS, [], []
-
],
-
['title', false, false, false, false, false, :any, false,
-
'document title ',
-
HTML_PCDATA, nil, I18N_ATTRS, [], []
-
],
-
['tr', false, false, false, false, false, :any, false,
-
'table row ',
-
TR_CONTENTS, 'td', TALIGN_ATTRS, BGCOLOR_ATTR, []
-
],
-
['tt', false, true, false, false, false, :any, true,
-
'teletype or monospaced text style',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
],
-
['u', false, true, false, false, true, :loose, true,
-
'underlined text style',
-
HTML_INLINE, nil, [], HTML_ATTRS, []
-
],
-
['ul', false, false, false, false, false, :any, false,
-
'unordered list ',
-
LI_ELT, 'li', HTML_ATTRS, UL_DEPR, []
-
],
-
['var', false, false, false, false, false, :any, true,
-
'instance of a variable or program argument',
-
HTML_INLINE, nil, HTML_ATTRS, [], []
-
]
-
1
].each do |descriptor|
-
92
name = descriptor[0]
-
-
92
begin
-
92
d = Desc.new(*descriptor)
-
-
# flatten all the attribute lists (Ruby1.9, *[a,b,c] can be
-
# used to flatten a literal list, but not in Ruby1.8).
-
92
d[:subelts] = d[:subelts].flatten
-
92
d[:attrs_opt] = d[:attrs_opt].flatten
-
92
d[:attrs_depr] = d[:attrs_depr].flatten
-
92
d[:attrs_req] = d[:attrs_req].flatten
-
rescue => e
-
p name
-
raise e
-
end
-
-
92
DefaultDescriptions[name] = d
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
class EntityDescription < Struct.new(:value, :name, :description); end
-
-
1
class EntityLookup
-
###
-
# Look up entity with +name+
-
1
def [] name
-
(val = get(name)) && val.value
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
###
-
# Nokogiri lets you write a SAX parser to process HTML but get HTML
-
# correction features.
-
#
-
# See Nokogiri::HTML::SAX::Parser for a basic example of using a
-
# SAX parser with HTML.
-
#
-
# For more information on SAX parsers, see Nokogiri::XML::SAX
-
1
module SAX
-
###
-
# This class lets you perform SAX style parsing on HTML with HTML
-
# error correction.
-
#
-
# Here is a basic usage example:
-
#
-
# class MyDoc < Nokogiri::XML::SAX::Document
-
# def start_element name, attributes = []
-
# puts "found a #{name}"
-
# end
-
# end
-
#
-
# parser = Nokogiri::HTML::SAX::Parser.new(MyDoc.new)
-
# parser.parse(File.read(ARGV[0], mode: 'rb'))
-
#
-
# For more information on SAX parsers, see Nokogiri::XML::SAX
-
1
class Parser < Nokogiri::XML::SAX::Parser
-
###
-
# Parse html stored in +data+ using +encoding+
-
1
def parse_memory data, encoding = 'UTF-8'
-
raise ArgumentError unless data
-
return unless data.length > 0
-
ctx = ParserContext.memory(data, encoding)
-
yield ctx if block_given?
-
ctx.parse_with self
-
end
-
-
###
-
# Parse a file with +filename+
-
1
def parse_file filename, encoding = 'UTF-8'
-
raise ArgumentError unless filename
-
raise Errno::ENOENT unless File.exist?(filename)
-
raise Errno::EISDIR if File.directory?(filename)
-
ctx = ParserContext.file(filename, encoding)
-
yield ctx if block_given?
-
ctx.parse_with self
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
module SAX
-
###
-
# Context for HTML SAX parsers. This class is usually not instantiated
-
# by the user. Instead, you should be looking at
-
# Nokogiri::HTML::SAX::Parser
-
1
class ParserContext < Nokogiri::XML::SAX::ParserContext
-
1
def self.new thing, encoding = 'UTF-8'
-
[:read, :close].all? { |x| thing.respond_to?(x) } ? super :
-
memory(thing, encoding)
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module HTML
-
1
module SAX
-
1
class PushParser
-
-
# The Nokogiri::HTML::SAX::Document on which the PushParser will be
-
# operating
-
1
attr_accessor :document
-
-
1
def initialize(doc = HTML::SAX::Document.new, file_name = nil, encoding = 'UTF-8')
-
@document = doc
-
@encoding = encoding
-
@sax_parser = HTML::SAX::Parser.new(doc, @encoding)
-
-
## Create our push parser context
-
initialize_native(@sax_parser, file_name, encoding)
-
end
-
-
###
-
# Write a +chunk+ of HTML to the PushParser. Any callback methods
-
# that can be called will be called immediately.
-
1
def write chunk, last_chunk = false
-
native_write(chunk, last_chunk)
-
end
-
1
alias :<< :write
-
-
###
-
# Finish the parsing. This method is only necessary for
-
# Nokogiri::HTML::SAX::Document#end_document to be called.
-
1
def finish
-
write '', true
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
class SyntaxError < ::StandardError
-
end
-
end
-
1
module Nokogiri
-
# The version of Nokogiri you are using
-
1
VERSION = '1.7.0.1'
-
-
1
class VersionInfo # :nodoc:
-
1
def jruby?
-
2
::JRUBY_VERSION if RUBY_PLATFORM == "java"
-
end
-
-
1
def engine
-
1
defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'mri'
-
end
-
-
1
def loaded_parser_version
-
LIBXML_PARSER_VERSION.scan(/^(\d+)(\d\d)(\d\d)(?!\d)/).first.collect{ |j|
-
9
j.to_i
-
3
}.join(".")
-
end
-
-
1
def compiled_parser_version
-
3
LIBXML_VERSION
-
end
-
-
1
def libxml2?
-
3
defined?(LIBXML_VERSION)
-
end
-
-
1
def libxml2_using_system?
-
! libxml2_using_packaged?
-
end
-
-
1
def libxml2_using_packaged?
-
1
NOKOGIRI_USE_PACKAGED_LIBRARIES
-
end
-
-
1
def warnings
-
2
return [] unless libxml2?
-
-
2
if compiled_parser_version != loaded_parser_version
-
["Nokogiri was built against LibXML version #{compiled_parser_version}, but has dynamically loaded #{loaded_parser_version}"]
-
else
-
2
[]
-
end
-
end
-
-
1
def to_hash
-
1
hash_info = {}
-
1
hash_info['warnings'] = []
-
1
hash_info['nokogiri'] = Nokogiri::VERSION
-
1
hash_info['ruby'] = {}
-
1
hash_info['ruby']['version'] = ::RUBY_VERSION
-
1
hash_info['ruby']['platform'] = ::RUBY_PLATFORM
-
1
hash_info['ruby']['description'] = ::RUBY_DESCRIPTION
-
1
hash_info['ruby']['engine'] = engine
-
1
hash_info['ruby']['jruby'] = jruby? if jruby?
-
-
1
if libxml2?
-
1
hash_info['libxml'] = {}
-
1
hash_info['libxml']['binding'] = 'extension'
-
1
if libxml2_using_packaged?
-
1
hash_info['libxml']['source'] = "packaged"
-
1
hash_info['libxml']['libxml2_path'] = NOKOGIRI_LIBXML2_PATH
-
1
hash_info['libxml']['libxslt_path'] = NOKOGIRI_LIBXSLT_PATH
-
1
hash_info['libxml']['libxml2_patches'] = NOKOGIRI_LIBXML2_PATCHES
-
1
hash_info['libxml']['libxslt_patches'] = NOKOGIRI_LIBXSLT_PATCHES
-
else
-
hash_info['libxml']['source'] = "system"
-
end
-
1
hash_info['libxml']['compiled'] = compiled_parser_version
-
1
hash_info['libxml']['loaded'] = loaded_parser_version
-
1
hash_info['warnings'] = warnings
-
elsif jruby?
-
hash_info['xerces'] = Nokogiri::XERCES_VERSION
-
hash_info['nekohtml'] = Nokogiri::NEKO_VERSION
-
end
-
-
1
hash_info
-
end
-
-
1
def to_markdown
-
begin
-
require 'psych'
-
rescue LoadError
-
end
-
require 'yaml'
-
"# Nokogiri (#{Nokogiri::VERSION})\n" +
-
YAML.dump(to_hash).each_line.map { |line| " #{line}" }.join
-
end
-
-
# FIXME: maybe switch to singleton?
-
1
@@instance = new
-
1
@@instance.warnings.each do |warning|
-
warn "WARNING: #{warning}"
-
end
-
3
def self.instance; @@instance; end
-
end
-
-
# More complete version information about libxml
-
1
VERSION_INFO = VersionInfo.instance.to_hash
-
-
1
def self.uses_libxml? # :nodoc:
-
VersionInfo.instance.libxml2?
-
end
-
-
1
def self.jruby? # :nodoc:
-
1
VersionInfo.instance.jruby?
-
end
-
end
-
1
require 'nokogiri/xml/pp'
-
1
require 'nokogiri/xml/parse_options'
-
1
require 'nokogiri/xml/sax'
-
1
require 'nokogiri/xml/searchable'
-
1
require 'nokogiri/xml/node'
-
1
require 'nokogiri/xml/attribute_decl'
-
1
require 'nokogiri/xml/element_decl'
-
1
require 'nokogiri/xml/element_content'
-
1
require 'nokogiri/xml/character_data'
-
1
require 'nokogiri/xml/namespace'
-
1
require 'nokogiri/xml/attr'
-
1
require 'nokogiri/xml/dtd'
-
1
require 'nokogiri/xml/cdata'
-
1
require 'nokogiri/xml/text'
-
1
require 'nokogiri/xml/document'
-
1
require 'nokogiri/xml/document_fragment'
-
1
require 'nokogiri/xml/processing_instruction'
-
1
require 'nokogiri/xml/node_set'
-
1
require 'nokogiri/xml/syntax_error'
-
1
require 'nokogiri/xml/xpath'
-
1
require 'nokogiri/xml/xpath_context'
-
1
require 'nokogiri/xml/builder'
-
1
require 'nokogiri/xml/reader'
-
1
require 'nokogiri/xml/notation'
-
1
require 'nokogiri/xml/entity_decl'
-
1
require 'nokogiri/xml/schema'
-
1
require 'nokogiri/xml/relax_ng'
-
-
1
module Nokogiri
-
1
class << self
-
###
-
# Parse XML. Convenience method for Nokogiri::XML::Document.parse
-
1
def XML thing, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_XML, &block
-
Nokogiri::XML::Document.parse(thing, url, encoding, options, &block)
-
end
-
end
-
-
1
module XML
-
# Original C14N 1.0 spec canonicalization
-
1
XML_C14N_1_0 = 0
-
# Exclusive C14N 1.0 spec canonicalization
-
1
XML_C14N_EXCLUSIVE_1_0 = 1
-
# C14N 1.1 spec canonicalization
-
1
XML_C14N_1_1 = 2
-
1
class << self
-
###
-
# Parse an XML document using the Nokogiri::XML::Reader API. See
-
# Nokogiri::XML::Reader for mor information
-
1
def Reader string_or_io, url = nil, encoding = nil, options = ParseOptions::STRICT
-
-
options = Nokogiri::XML::ParseOptions.new(options) if Integer === options
-
# Give the options to the user
-
yield options if block_given?
-
-
if string_or_io.respond_to? :read
-
return Reader.from_io(string_or_io, url, encoding, options.to_i)
-
end
-
Reader.from_memory(string_or_io, url, encoding, options.to_i)
-
end
-
-
###
-
# Parse XML. Convenience method for Nokogiri::XML::Document.parse
-
1
def parse thing, url = nil, encoding = nil, options = ParseOptions::DEFAULT_XML, &block
-
Document.parse(thing, url, encoding, options, &block)
-
end
-
-
####
-
# Parse a fragment from +string+ in to a NodeSet.
-
1
def fragment string
-
XML::DocumentFragment.parse(string)
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class Attr < Node
-
1
alias :value :content
-
1
alias :to_s :content
-
1
alias :content= :value=
-
-
1
private
-
1
def inspect_attributes
-
[:name, :namespace, :value]
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# Represents an attribute declaration in a DTD
-
1
class AttributeDecl < Nokogiri::XML::Node
-
1
undef_method :attribute_nodes
-
1
undef_method :attributes
-
1
undef_method :content
-
1
undef_method :namespace
-
1
undef_method :namespace_definitions
-
1
undef_method :line if method_defined?(:line)
-
-
1
def inspect
-
"#<#{self.class.name}:#{sprintf("0x%x", object_id)} #{to_s.inspect}>"
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# Nokogiri builder can be used for building XML and HTML documents.
-
#
-
# == Synopsis:
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.root {
-
# xml.products {
-
# xml.widget {
-
# xml.id_ "10"
-
# xml.name "Awesome widget"
-
# }
-
# }
-
# }
-
# end
-
# puts builder.to_xml
-
#
-
# Will output:
-
#
-
# <?xml version="1.0"?>
-
# <root>
-
# <products>
-
# <widget>
-
# <id>10</id>
-
# <name>Awesome widget</name>
-
# </widget>
-
# </products>
-
# </root>
-
#
-
#
-
# === Builder scope
-
#
-
# The builder allows two forms. When the builder is supplied with a block
-
# that has a parameter, the outside scope is maintained. This means you
-
# can access variables that are outside your builder. If you don't need
-
# outside scope, you can use the builder without the "xml" prefix like
-
# this:
-
#
-
# builder = Nokogiri::XML::Builder.new do
-
# root {
-
# products {
-
# widget {
-
# id_ "10"
-
# name "Awesome widget"
-
# }
-
# }
-
# }
-
# end
-
#
-
# == Special Tags
-
#
-
# The builder works by taking advantage of method_missing. Unfortunately
-
# some methods are defined in ruby that are difficult or dangerous to
-
# remove. You may want to create tags with the name "type", "class", and
-
# "id" for example. In that case, you can use an underscore to
-
# disambiguate your tag name from the method call.
-
#
-
# Here is an example of using the underscore to disambiguate tag names from
-
# ruby methods:
-
#
-
# @objects = [Object.new, Object.new, Object.new]
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.root {
-
# xml.objects {
-
# @objects.each do |o|
-
# xml.object {
-
# xml.type_ o.type
-
# xml.class_ o.class.name
-
# xml.id_ o.id
-
# }
-
# end
-
# }
-
# }
-
# end
-
# puts builder.to_xml
-
#
-
# The underscore may be used with any tag name, and the last underscore
-
# will just be removed. This code will output the following XML:
-
#
-
# <?xml version="1.0"?>
-
# <root>
-
# <objects>
-
# <object>
-
# <type>Object</type>
-
# <class>Object</class>
-
# <id>48390</id>
-
# </object>
-
# <object>
-
# <type>Object</type>
-
# <class>Object</class>
-
# <id>48380</id>
-
# </object>
-
# <object>
-
# <type>Object</type>
-
# <class>Object</class>
-
# <id>48370</id>
-
# </object>
-
# </objects>
-
# </root>
-
#
-
# == Tag Attributes
-
#
-
# Tag attributes may be supplied as method arguments. Here is our
-
# previous example, but using attributes rather than tags:
-
#
-
# @objects = [Object.new, Object.new, Object.new]
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.root {
-
# xml.objects {
-
# @objects.each do |o|
-
# xml.object(:type => o.type, :class => o.class, :id => o.id)
-
# end
-
# }
-
# }
-
# end
-
# puts builder.to_xml
-
#
-
# === Tag Attribute Short Cuts
-
#
-
# A couple attribute short cuts are available when building tags. The
-
# short cuts are available by special method calls when building a tag.
-
#
-
# This example builds an "object" tag with the class attribute "classy"
-
# and the id of "thing":
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.root {
-
# xml.objects {
-
# xml.object.classy.thing!
-
# }
-
# }
-
# end
-
# puts builder.to_xml
-
#
-
# Which will output:
-
#
-
# <?xml version="1.0"?>
-
# <root>
-
# <objects>
-
# <object class="classy" id="thing"/>
-
# </objects>
-
# </root>
-
#
-
# All other options are still supported with this syntax, including
-
# blocks and extra tag attributes.
-
#
-
# == Namespaces
-
#
-
# Namespaces are added similarly to attributes. Nokogiri::XML::Builder
-
# assumes that when an attribute starts with "xmlns", it is meant to be
-
# a namespace:
-
#
-
# builder = Nokogiri::XML::Builder.new { |xml|
-
# xml.root('xmlns' => 'default', 'xmlns:foo' => 'bar') do
-
# xml.tenderlove
-
# end
-
# }
-
# puts builder.to_xml
-
#
-
# Will output XML like this:
-
#
-
# <?xml version="1.0"?>
-
# <root xmlns:foo="bar" xmlns="default">
-
# <tenderlove/>
-
# </root>
-
#
-
# === Referencing declared namespaces
-
#
-
# Tags that reference non-default namespaces (i.e. a tag "foo:bar") can be
-
# built by using the Nokogiri::XML::Builder#[] method.
-
#
-
# For example:
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.root('xmlns:foo' => 'bar') {
-
# xml.objects {
-
# xml['foo'].object.classy.thing!
-
# }
-
# }
-
# end
-
# puts builder.to_xml
-
#
-
# Will output this XML:
-
#
-
# <?xml version="1.0"?>
-
# <root xmlns:foo="bar">
-
# <objects>
-
# <foo:object class="classy" id="thing"/>
-
# </objects>
-
# </root>
-
#
-
# Note the "foo:object" tag.
-
#
-
# == Document Types
-
#
-
# To create a document type (DTD), access use the Builder#doc method to get
-
# the current context document. Then call Node#create_internal_subset to
-
# create the DTD node.
-
#
-
# For example, this Ruby:
-
#
-
# builder = Nokogiri::XML::Builder.new do |xml|
-
# xml.doc.create_internal_subset(
-
# 'html',
-
# "-//W3C//DTD HTML 4.01 Transitional//EN",
-
# "http://www.w3.org/TR/html4/loose.dtd"
-
# )
-
# xml.root do
-
# xml.foo
-
# end
-
# end
-
#
-
# puts builder.to_xml
-
#
-
# Will output this xml:
-
#
-
# <?xml version="1.0"?>
-
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
# <root>
-
# <foo/>
-
# </root>
-
#
-
1
class Builder
-
# The current Document object being built
-
1
attr_accessor :doc
-
-
# The parent of the current node being built
-
1
attr_accessor :parent
-
-
# A context object for use when the block has no arguments
-
1
attr_accessor :context
-
-
1
attr_accessor :arity # :nodoc:
-
-
###
-
# Create a builder with an existing root object. This is for use when
-
# you have an existing document that you would like to augment with
-
# builder methods. The builder context created will start with the
-
# given +root+ node.
-
#
-
# For example:
-
#
-
# doc = Nokogiri::XML(open('somedoc.xml'))
-
# Nokogiri::XML::Builder.with(doc.at('some_tag')) do |xml|
-
# # ... Use normal builder methods here ...
-
# xml.awesome # add the "awesome" tag below "some_tag"
-
# end
-
#
-
1
def self.with root, &block
-
new({}, root, &block)
-
end
-
-
###
-
# Create a new Builder object. +options+ are sent to the top level
-
# Document that is being built.
-
#
-
# Building a document with a particular encoding for example:
-
#
-
# Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
-
# ...
-
# end
-
1
def initialize options = {}, root = nil, &block
-
-
if root
-
@doc = root.document
-
@parent = root
-
else
-
namespace = self.class.name.split('::')
-
namespace[-1] = 'Document'
-
@doc = eval(namespace.join('::')).new
-
@parent = @doc
-
end
-
-
@context = nil
-
@arity = nil
-
@ns = nil
-
-
options.each do |k,v|
-
@doc.send(:"#{k}=", v)
-
end
-
-
return unless block_given?
-
-
@arity = block.arity
-
if @arity <= 0
-
@context = eval('self', block.binding)
-
instance_eval(&block)
-
else
-
yield self
-
end
-
-
@parent = @doc
-
end
-
-
###
-
# Create a Text Node with content of +string+
-
1
def text string
-
insert @doc.create_text_node(string)
-
end
-
-
###
-
# Create a CDATA Node with content of +string+
-
1
def cdata string
-
insert doc.create_cdata(string)
-
end
-
-
###
-
# Create a Comment Node with content of +string+
-
1
def comment string
-
insert doc.create_comment(string)
-
end
-
-
###
-
# Build a tag that is associated with namespace +ns+. Raises an
-
# ArgumentError if +ns+ has not been defined higher in the tree.
-
1
def [] ns
-
if @parent != @doc
-
@ns = @parent.namespace_definitions.find { |x| x.prefix == ns.to_s }
-
end
-
return self if @ns
-
-
@parent.ancestors.each do |a|
-
next if a == doc
-
@ns = a.namespace_definitions.find { |x| x.prefix == ns.to_s }
-
return self if @ns
-
end
-
-
@ns = { :pending => ns.to_s }
-
return self
-
end
-
-
###
-
# Convert this Builder object to XML
-
1
def to_xml(*args)
-
if Nokogiri.jruby?
-
options = args.first.is_a?(Hash) ? args.shift : {}
-
if !options[:save_with]
-
options[:save_with] = Node::SaveOptions::AS_BUILDER
-
end
-
args.insert(0, options)
-
end
-
@doc.to_xml(*args)
-
end
-
-
###
-
# Append the given raw XML +string+ to the document
-
1
def << string
-
@doc.fragment(string).children.each { |x| insert(x) }
-
end
-
-
1
def method_missing method, *args, &block # :nodoc:
-
if @context && @context.respond_to?(method)
-
@context.send(method, *args, &block)
-
else
-
node = @doc.create_element(method.to_s.sub(/[_!]$/, ''),*args) { |n|
-
# Set up the namespace
-
if @ns.is_a? Nokogiri::XML::Namespace
-
n.namespace = @ns
-
@ns = nil
-
end
-
}
-
-
if @ns.is_a? Hash
-
node.namespace = node.namespace_definitions.find { |x| x.prefix == @ns[:pending] }
-
if node.namespace.nil?
-
raise ArgumentError, "Namespace #{@ns[:pending]} has not been defined"
-
end
-
@ns = nil
-
end
-
-
insert(node, &block)
-
end
-
end
-
-
1
private
-
###
-
# Insert +node+ as a child of the current Node
-
1
def insert(node, &block)
-
node = @parent.add_child(node)
-
if block_given?
-
old_parent = @parent
-
@parent = node
-
@arity ||= block.arity
-
if @arity <= 0
-
instance_eval(&block)
-
else
-
block.call(self)
-
end
-
@parent = old_parent
-
end
-
NodeBuilder.new(node, self)
-
end
-
-
1
class NodeBuilder # :nodoc:
-
1
def initialize node, doc_builder
-
@node = node
-
@doc_builder = doc_builder
-
end
-
-
1
def []= k, v
-
@node[k] = v
-
end
-
-
1
def [] k
-
@node[k]
-
end
-
-
1
def method_missing(method, *args, &block)
-
opts = args.last.is_a?(Hash) ? args.pop : {}
-
case method.to_s
-
when /^(.*)!$/
-
@node['id'] = $1
-
@node.content = args.first if args.first
-
when /^(.*)=/
-
@node[$1] = args.first
-
else
-
@node['class'] =
-
((@node['class'] || '').split(/\s/) + [method.to_s]).join(' ')
-
@node.content = args.first if args.first
-
end
-
-
# Assign any extra options
-
opts.each do |k,v|
-
@node[k.to_s] = ((@node[k.to_s] || '').split(/\s/) + [v]).join(' ')
-
end
-
-
if block_given?
-
old_parent = @doc_builder.parent
-
@doc_builder.parent = @node
-
value = @doc_builder.instance_eval(&block)
-
@doc_builder.parent = old_parent
-
return value
-
end
-
self
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class CDATA < Nokogiri::XML::Text
-
###
-
# Get the name of this CDATA node
-
1
def name
-
'#cdata-section'
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class CharacterData < Nokogiri::XML::Node
-
1
include Nokogiri::XML::PP::CharacterData
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
##
-
# Nokogiri::XML::Document is the main entry point for dealing with
-
# XML documents. The Document is created by parsing an XML document.
-
# See Nokogiri::XML::Document.parse() for more information on parsing.
-
#
-
# For searching a Document, see Nokogiri::XML::Searchable#css and
-
# Nokogiri::XML::Searchable#xpath
-
#
-
1
class Document < Nokogiri::XML::Node
-
# I'm ignoring unicode characters here.
-
# See http://www.w3.org/TR/REC-xml-names/#ns-decl for more details.
-
1
NCNAME_START_CHAR = "A-Za-z_"
-
1
NCNAME_CHAR = NCNAME_START_CHAR + "\\-.0-9"
-
1
NCNAME_RE = /^xmlns(:[#{NCNAME_START_CHAR}][#{NCNAME_CHAR}]*)?$/
-
-
##
-
# Parse an XML file.
-
#
-
# +string_or_io+ may be a String, or any object that responds to
-
# _read_ and _close_ such as an IO, or StringIO.
-
#
-
# +url+ (optional) is the URI where this document is located.
-
#
-
# +encoding+ (optional) is the encoding that should be used when processing
-
# the document.
-
#
-
# +options+ (optional) is a configuration object that sets options during
-
# parsing, such as Nokogiri::XML::ParseOptions::RECOVER. See the
-
# Nokogiri::XML::ParseOptions for more information.
-
#
-
# +block+ (optional) is passed a configuration object on which
-
# parse options may be set.
-
#
-
# When parsing untrusted documents, it's recommended that the
-
# +nonet+ option be used, as shown in this example code:
-
#
-
# Nokogiri::XML::Document.parse(xml_string) { |config| config.nonet }
-
#
-
# Nokogiri.XML() is a convenience method which will call this method.
-
#
-
1
def self.parse string_or_io, url = nil, encoding = nil, options = ParseOptions::DEFAULT_XML, &block
-
options = Nokogiri::XML::ParseOptions.new(options) if Integer === options
-
# Give the options to the user
-
yield options if block_given?
-
-
if empty_doc?(string_or_io)
-
if options.strict?
-
raise Nokogiri::XML::SyntaxError.new("Empty document")
-
else
-
return encoding ? new.tap { |i| i.encoding = encoding } : new
-
end
-
end
-
-
doc = if string_or_io.respond_to?(:read)
-
url ||= string_or_io.respond_to?(:path) ? string_or_io.path : nil
-
read_io(string_or_io, url, encoding, options.to_i)
-
else
-
# read_memory pukes on empty docs
-
read_memory(string_or_io, url, encoding, options.to_i)
-
end
-
-
# do xinclude processing
-
doc.do_xinclude(options) if options.xinclude?
-
-
return doc
-
end
-
-
# A list of Nokogiri::XML::SyntaxError found when parsing a document
-
1
attr_accessor :errors
-
-
1
def initialize *args # :nodoc:
-
@errors = []
-
@decorators = nil
-
end
-
-
##
-
# Create an element with +name+, and optionally setting the content and attributes.
-
#
-
# doc.create_element "div" # <div></div>
-
# doc.create_element "div", :class => "container" # <div class='container'></div>
-
# doc.create_element "div", "contents" # <div>contents</div>
-
# doc.create_element "div", "contents", :class => "container" # <div class='container'>contents</div>
-
# doc.create_element "div" { |node| node['class'] = "container" } # <div class='container'></div>
-
#
-
1
def create_element name, *args, &block
-
elm = Nokogiri::XML::Element.new(name, self, &block)
-
args.each do |arg|
-
case arg
-
when Hash
-
arg.each { |k,v|
-
key = k.to_s
-
if key =~ NCNAME_RE
-
ns_name = key.split(":", 2)[1]
-
elm.add_namespace_definition ns_name, v
-
else
-
elm[k.to_s] = v.to_s
-
end
-
}
-
else
-
elm.content = arg
-
end
-
end
-
if ns = elm.namespace_definitions.find { |n| n.prefix.nil? or n.prefix == '' }
-
elm.namespace = ns
-
end
-
elm
-
end
-
-
# Create a Text Node with +string+
-
1
def create_text_node string, &block
-
Nokogiri::XML::Text.new string.to_s, self, &block
-
end
-
-
# Create a CDATA Node containing +string+
-
1
def create_cdata string, &block
-
Nokogiri::XML::CDATA.new self, string.to_s, &block
-
end
-
-
# Create a Comment Node containing +string+
-
1
def create_comment string, &block
-
Nokogiri::XML::Comment.new self, string.to_s, &block
-
end
-
-
# The name of this document. Always returns "document"
-
1
def name
-
'document'
-
end
-
-
# A reference to +self+
-
1
def document
-
self
-
end
-
-
##
-
# Recursively get all namespaces from this node and its subtree and
-
# return them as a hash.
-
#
-
# For example, given this document:
-
#
-
# <root xmlns:foo="bar">
-
# <bar xmlns:hello="world" />
-
# </root>
-
#
-
# This method will return:
-
#
-
# { 'xmlns:foo' => 'bar', 'xmlns:hello' => 'world' }
-
#
-
# WARNING: this method will clobber duplicate names in the keys.
-
# For example, given this document:
-
#
-
# <root xmlns:foo="bar">
-
# <bar xmlns:foo="baz" />
-
# </root>
-
#
-
# The hash returned will look like this: { 'xmlns:foo' => 'bar' }
-
#
-
# Non-prefixed default namespaces (as in "xmlns=") are not included
-
# in the hash.
-
#
-
# Note that this method does an xpath lookup for nodes with
-
# namespaces, and as a result the order may be dependent on the
-
# implementation of the underlying XML library.
-
#
-
1
def collect_namespaces
-
xpath("//namespace::*").inject({}) do |hash, ns|
-
hash[["xmlns",ns.prefix].compact.join(":")] = ns.href if ns.prefix != "xml"
-
hash
-
end
-
end
-
-
# Get the list of decorators given +key+
-
1
def decorators key
-
@decorators ||= Hash.new
-
@decorators[key] ||= []
-
end
-
-
##
-
# Validate this Document against it's DTD. Returns a list of errors on
-
# the document or +nil+ when there is no DTD.
-
1
def validate
-
return nil unless internal_subset
-
internal_subset.validate self
-
end
-
-
##
-
# Explore a document with shortcut methods. See Nokogiri::Slop for details.
-
#
-
# Note that any nodes that have been instantiated before #slop!
-
# is called will not be decorated with sloppy behavior. So, if you're in
-
# irb, the preferred idiom is:
-
#
-
# irb> doc = Nokogiri::Slop my_markup
-
#
-
# and not
-
#
-
# irb> doc = Nokogiri::HTML my_markup
-
# ... followed by irb's implicit inspect (and therefore instantiation of every node) ...
-
# irb> doc.slop!
-
# ... which does absolutely nothing.
-
#
-
1
def slop!
-
unless decorators(XML::Node).include? Nokogiri::Decorators::Slop
-
decorators(XML::Node) << Nokogiri::Decorators::Slop
-
decorate!
-
end
-
-
self
-
end
-
-
##
-
# Apply any decorators to +node+
-
1
def decorate node
-
return unless @decorators
-
@decorators.each { |klass,list|
-
next unless node.is_a?(klass)
-
list.each { |moodule| node.extend(moodule) }
-
}
-
end
-
-
1
alias :to_xml :serialize
-
1
alias :clone :dup
-
-
# Get the hash of namespaces on the root Nokogiri::XML::Node
-
1
def namespaces
-
root ? root.namespaces : {}
-
end
-
-
##
-
# Create a Nokogiri::XML::DocumentFragment from +tags+
-
# Returns an empty fragment if +tags+ is nil.
-
1
def fragment tags = nil
-
DocumentFragment.new(self, tags, self.root)
-
end
-
-
1
undef_method :swap, :parent, :namespace, :default_namespace=
-
1
undef_method :add_namespace_definition, :attributes
-
1
undef_method :namespace_definitions, :line, :add_namespace
-
-
1
def add_child node_or_tags
-
raise "Document already has a root node" if root && root.name != 'nokogiri_text_wrapper'
-
node_or_tags = coerce(node_or_tags)
-
if node_or_tags.is_a?(XML::NodeSet)
-
raise "Document cannot have multiple root nodes" if node_or_tags.size > 1
-
super(node_or_tags.first)
-
else
-
super
-
end
-
end
-
1
alias :<< :add_child
-
-
##
-
# +JRuby+
-
# Wraps Java's org.w3c.dom.document and returns Nokogiri::XML::Document
-
1
def self.wrap document
-
raise "JRuby only method" unless Nokogiri.jruby?
-
return wrapJavaDocument(document)
-
end
-
-
##
-
# +JRuby+
-
# Returns Java's org.w3c.dom.document of this Document.
-
1
def to_java
-
raise "JRuby only method" unless Nokogiri.jruby?
-
return toJavaDocument()
-
end
-
-
1
private
-
1
def self.empty_doc? string_or_io
-
string_or_io.nil? ||
-
(string_or_io.respond_to?(:empty?) && string_or_io.empty?) ||
-
(string_or_io.respond_to?(:eof?) && string_or_io.eof?)
-
end
-
-
1
def implied_xpath_contexts # :nodoc:
-
["//"]
-
end
-
-
1
def inspect_attributes
-
[:name, :children]
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class DocumentFragment < Nokogiri::XML::Node
-
##
-
# Create a new DocumentFragment from +tags+.
-
#
-
# If +ctx+ is present, it is used as a context node for the
-
# subtree created, e.g., namespaces will be resolved relative
-
# to +ctx+.
-
1
def initialize document, tags = nil, ctx = nil
-
return self unless tags
-
-
children = if ctx
-
# Fix for issue#490
-
if Nokogiri.jruby?
-
# fix for issue #770
-
ctx.parse("<root #{namespace_declarations(ctx)}>#{tags}</root>").children
-
else
-
ctx.parse(tags)
-
end
-
else
-
XML::Document.parse("<root>#{tags}</root>") \
-
.xpath("/root/node()")
-
end
-
children.each { |child| child.parent = self }
-
end
-
-
###
-
# return the name for DocumentFragment
-
1
def name
-
'#document-fragment'
-
end
-
-
###
-
# Convert this DocumentFragment to a string
-
1
def to_s
-
children.to_s
-
end
-
-
###
-
# Convert this DocumentFragment to html
-
# See Nokogiri::XML::NodeSet#to_html
-
1
def to_html *args
-
if Nokogiri.jruby?
-
options = args.first.is_a?(Hash) ? args.shift : {}
-
if !options[:save_with]
-
options[:save_with] = Node::SaveOptions::NO_DECLARATION | Node::SaveOptions::NO_EMPTY_TAGS | Node::SaveOptions::AS_HTML
-
end
-
args.insert(0, options)
-
end
-
children.to_html(*args)
-
end
-
-
###
-
# Convert this DocumentFragment to xhtml
-
# See Nokogiri::XML::NodeSet#to_xhtml
-
1
def to_xhtml *args
-
if Nokogiri.jruby?
-
options = args.first.is_a?(Hash) ? args.shift : {}
-
if !options[:save_with]
-
options[:save_with] = Node::SaveOptions::NO_DECLARATION | Node::SaveOptions::NO_EMPTY_TAGS | Node::SaveOptions::AS_XHTML
-
end
-
args.insert(0, options)
-
end
-
children.to_xhtml(*args)
-
end
-
-
###
-
# Convert this DocumentFragment to xml
-
# See Nokogiri::XML::NodeSet#to_xml
-
1
def to_xml *args
-
children.to_xml(*args)
-
end
-
-
###
-
# call-seq: css *rules, [namespace-bindings, custom-pseudo-class]
-
#
-
# Search this fragment for CSS +rules+. +rules+ must be one or more CSS
-
# selectors. For example:
-
#
-
# For more information see Nokogiri::XML::Searchable#css
-
1
def css *args
-
if children.any?
-
children.css(*args) # 'children' is a smell here
-
else
-
NodeSet.new(document)
-
end
-
end
-
-
#
-
# NOTE that we don't delegate #xpath to children ... another smell.
-
# def xpath ; end
-
#
-
-
###
-
# call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class]
-
#
-
# Search this fragment for +paths+. +paths+ must be one or more XPath or CSS queries.
-
#
-
# For more information see Nokogiri::XML::Searchable#search
-
1
def search *rules
-
rules, handler, ns, binds = extract_params(rules)
-
-
rules.inject(NodeSet.new(document)) do |set, rule|
-
set += if rule =~ Searchable::LOOKS_LIKE_XPATH
-
xpath(*([rule, ns, handler, binds].compact))
-
else
-
children.css(*([rule, ns, handler].compact)) # 'children' is a smell here
-
end
-
end
-
end
-
-
1
alias :serialize :to_s
-
-
1
class << self
-
####
-
# Create a Nokogiri::XML::DocumentFragment from +tags+
-
1
def parse tags
-
self.new(XML::Document.new, tags)
-
end
-
end
-
-
# A list of Nokogiri::XML::SyntaxError found when parsing a document
-
1
def errors
-
document.errors
-
end
-
-
1
def errors= things # :nodoc:
-
document.errors = things
-
end
-
-
1
private
-
-
# fix for issue 770
-
1
def namespace_declarations ctx
-
ctx.namespace_scopes.map do |namespace|
-
prefix = namespace.prefix.nil? ? "" : ":#{namespace.prefix}"
-
%Q{xmlns#{prefix}="#{namespace.href}"}
-
end.join ' '
-
end
-
-
1
def coerce data
-
return super unless String === data
-
-
document.fragment(data).children
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class DTD < Nokogiri::XML::Node
-
1
undef_method :attribute_nodes
-
1
undef_method :values
-
1
undef_method :content
-
1
undef_method :namespace
-
1
undef_method :namespace_definitions
-
1
undef_method :line if method_defined?(:line)
-
-
1
def keys
-
attributes.keys
-
end
-
-
1
def each
-
attributes.each do |key, value|
-
yield([key, value])
-
end
-
end
-
-
1
def html_dtd?
-
name.casecmp('html').zero?
-
end
-
-
1
def html5_dtd?
-
html_dtd? &&
-
external_id.nil? &&
-
(system_id.nil? || system_id == 'about:legacy-compat')
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# Represents the allowed content in an Element Declaration inside a DTD:
-
#
-
# <?xml version="1.0"?><?TEST-STYLE PIDATA?>
-
# <!DOCTYPE staff SYSTEM "staff.dtd" [
-
# <!ELEMENT div1 (head, (p | list | note)*, div2*)>
-
# ]>
-
# </root>
-
#
-
# ElementContent represents the tree inside the <!ELEMENT> tag shown above
-
# that lists the possible content for the div1 tag.
-
1
class ElementContent
-
# Possible definitions of type
-
1
PCDATA = 1
-
1
ELEMENT = 2
-
1
SEQ = 3
-
1
OR = 4
-
-
# Possible content occurrences
-
1
ONCE = 1
-
1
OPT = 2
-
1
MULT = 3
-
1
PLUS = 4
-
-
1
attr_reader :document
-
-
###
-
# Get the children of this ElementContent node
-
1
def children
-
[c1, c2].compact
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class ElementDecl < Nokogiri::XML::Node
-
1
undef_method :namespace
-
1
undef_method :namespace_definitions
-
1
undef_method :line if method_defined?(:line)
-
-
1
def inspect
-
"#<#{self.class.name}:#{sprintf("0x%x", object_id)} #{to_s.inspect}>"
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class EntityDecl < Nokogiri::XML::Node
-
1
undef_method :attribute_nodes
-
1
undef_method :attributes
-
1
undef_method :namespace
-
1
undef_method :namespace_definitions
-
1
undef_method :line if method_defined?(:line)
-
-
1
def self.new name, doc, *args
-
doc.create_entity(name, *args)
-
end
-
-
1
def inspect
-
"#<#{self.class.name}:#{sprintf("0x%x", object_id)} #{to_s.inspect}>"
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class Namespace
-
1
include Nokogiri::XML::PP::Node
-
1
attr_reader :document
-
-
1
private
-
1
def inspect_attributes
-
[:prefix, :href]
-
end
-
end
-
end
-
end
-
# encoding: UTF-8
-
1
require 'stringio'
-
1
require 'nokogiri/xml/node/save_options'
-
-
1
module Nokogiri
-
1
module XML
-
####
-
# Nokogiri::XML::Node is your window to the fun filled world of dealing
-
# with XML and HTML tags. A Nokogiri::XML::Node may be treated similarly
-
# to a hash with regard to attributes. For example (from irb):
-
#
-
# irb(main):004:0> node
-
# => <a href="#foo" id="link">link</a>
-
# irb(main):005:0> node['href']
-
# => "#foo"
-
# irb(main):006:0> node.keys
-
# => ["href", "id"]
-
# irb(main):007:0> node.values
-
# => ["#foo", "link"]
-
# irb(main):008:0> node['class'] = 'green'
-
# => "green"
-
# irb(main):009:0> node
-
# => <a href="#foo" id="link" class="green">link</a>
-
# irb(main):010:0>
-
#
-
# See Nokogiri::XML::Node#[] and Nokogiri::XML#[]= for more information.
-
#
-
# Nokogiri::XML::Node also has methods that let you move around your
-
# tree. For navigating your tree, see:
-
#
-
# * Nokogiri::XML::Node#parent
-
# * Nokogiri::XML::Node#children
-
# * Nokogiri::XML::Node#next
-
# * Nokogiri::XML::Node#previous
-
#
-
#
-
# When printing or otherwise emitting a document or a node (and
-
# its subtree), there are a few methods you might want to use:
-
#
-
# * content, text, inner_text, to_str: emit plaintext
-
#
-
# These methods will all emit the plaintext version of your
-
# document, meaning that entities will be replaced (e.g., "<"
-
# will be replaced with "<"), meaning that any sanitizing will
-
# likely be un-done in the output.
-
#
-
# * to_s, to_xml, to_html, inner_html: emit well-formed markup
-
#
-
# These methods will all emit properly-escaped markup, meaning
-
# that it's suitable for consumption by browsers, parsers, etc.
-
#
-
# You may search this node's subtree using Searchable#xpath and Searchable#css
-
1
class Node
-
1
include Nokogiri::XML::PP::Node
-
1
include Nokogiri::XML::Searchable
-
1
include Enumerable
-
-
# Element node type, see Nokogiri::XML::Node#element?
-
1
ELEMENT_NODE = 1
-
# Attribute node type
-
1
ATTRIBUTE_NODE = 2
-
# Text node type, see Nokogiri::XML::Node#text?
-
1
TEXT_NODE = 3
-
# CDATA node type, see Nokogiri::XML::Node#cdata?
-
1
CDATA_SECTION_NODE = 4
-
# Entity reference node type
-
1
ENTITY_REF_NODE = 5
-
# Entity node type
-
1
ENTITY_NODE = 6
-
# PI node type
-
1
PI_NODE = 7
-
# Comment node type, see Nokogiri::XML::Node#comment?
-
1
COMMENT_NODE = 8
-
# Document node type, see Nokogiri::XML::Node#xml?
-
1
DOCUMENT_NODE = 9
-
# Document type node type
-
1
DOCUMENT_TYPE_NODE = 10
-
# Document fragment node type
-
1
DOCUMENT_FRAG_NODE = 11
-
# Notation node type
-
1
NOTATION_NODE = 12
-
# HTML document node type, see Nokogiri::XML::Node#html?
-
1
HTML_DOCUMENT_NODE = 13
-
# DTD node type
-
1
DTD_NODE = 14
-
# Element declaration type
-
1
ELEMENT_DECL = 15
-
# Attribute declaration type
-
1
ATTRIBUTE_DECL = 16
-
# Entity declaration type
-
1
ENTITY_DECL = 17
-
# Namespace declaration type
-
1
NAMESPACE_DECL = 18
-
# XInclude start type
-
1
XINCLUDE_START = 19
-
# XInclude end type
-
1
XINCLUDE_END = 20
-
# DOCB document node type
-
1
DOCB_DOCUMENT_NODE = 21
-
-
1
def initialize name, document # :nodoc:
-
# ... Ya. This is empty on purpose.
-
end
-
-
###
-
# Decorate this node with the decorators set up in this node's Document
-
1
def decorate!
-
document.decorate(self)
-
end
-
-
###
-
# Search this node's immediate children using CSS selector +selector+
-
1
def > selector
-
ns = document.root.namespaces
-
xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first
-
end
-
-
###
-
# Get the attribute value for the attribute +name+
-
1
def [] name
-
get(name.to_s)
-
end
-
-
###
-
# Set the attribute value for the attribute +name+ to +value+
-
1
def []= name, value
-
set name.to_s, value.to_s
-
end
-
-
###
-
# Add +node_or_tags+ as a child of this Node.
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +<<+.
-
1
def add_child node_or_tags
-
node_or_tags = coerce(node_or_tags)
-
if node_or_tags.is_a?(XML::NodeSet)
-
node_or_tags.each { |n| add_child_node_and_reparent_attrs n }
-
else
-
add_child_node_and_reparent_attrs node_or_tags
-
end
-
node_or_tags
-
end
-
-
###
-
# Add +node_or_tags+ as the first child of this Node.
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +add_child+.
-
1
def prepend_child node_or_tags
-
if first = children.first
-
# Mimic the error add_child would raise.
-
raise RuntimeError, "Document already has a root node" if document? && !node_or_tags.processing_instruction?
-
first.__send__(:add_sibling, :previous, node_or_tags)
-
else
-
add_child(node_or_tags)
-
end
-
end
-
-
###
-
# Add +node_or_tags+ as a child of this Node.
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns self, to support chaining of calls (e.g., root << child1 << child2)
-
#
-
# Also see related method +add_child+.
-
1
def << node_or_tags
-
add_child node_or_tags
-
self
-
end
-
###
-
# Insert +node_or_tags+ before this Node (as a sibling).
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +before+.
-
1
def add_previous_sibling node_or_tags
-
raise ArgumentError.new("A document may not have multiple root nodes.") if (parent && parent.document?) && !node_or_tags.processing_instruction?
-
-
add_sibling :previous, node_or_tags
-
end
-
-
###
-
# Insert +node_or_tags+ after this Node (as a sibling).
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +after+.
-
1
def add_next_sibling node_or_tags
-
raise ArgumentError.new("A document may not have multiple root nodes.") if (parent && parent.document?) && !node_or_tags.processing_instruction?
-
-
add_sibling :next, node_or_tags
-
end
-
-
####
-
# Insert +node_or_tags+ before this node (as a sibling).
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns self, to support chaining of calls.
-
#
-
# Also see related method +add_previous_sibling+.
-
1
def before node_or_tags
-
add_previous_sibling node_or_tags
-
self
-
end
-
-
####
-
# Insert +node_or_tags+ after this node (as a sibling).
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup.
-
#
-
# Returns self, to support chaining of calls.
-
#
-
# Also see related method +add_next_sibling+.
-
1
def after node_or_tags
-
add_next_sibling node_or_tags
-
self
-
end
-
-
####
-
# Set the inner html for this Node to +node_or_tags+
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup.
-
#
-
# Returns self.
-
#
-
# Also see related method +children=+
-
1
def inner_html= node_or_tags
-
self.children = node_or_tags
-
self
-
end
-
-
####
-
# Set the inner html for this Node +node_or_tags+
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +inner_html=+
-
1
def children= node_or_tags
-
node_or_tags = coerce(node_or_tags)
-
children.unlink
-
if node_or_tags.is_a?(XML::NodeSet)
-
node_or_tags.each { |n| add_child_node_and_reparent_attrs n }
-
else
-
add_child_node_and_reparent_attrs node_or_tags
-
end
-
node_or_tags
-
end
-
-
####
-
# Replace this Node with +node_or_tags+.
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string).
-
#
-
# Also see related method +swap+.
-
1
def replace node_or_tags
-
# We cannot replace a text node directly, otherwise libxml will return
-
# an internal error at parser.c:13031, I don't know exactly why
-
# libxml is trying to find a parent node that is an element or document
-
# so I can't tell if this is bug in libxml or not. issue #775.
-
if text?
-
replacee = Nokogiri::XML::Node.new 'dummy', document
-
add_previous_sibling_node replacee
-
unlink
-
return replacee.replace node_or_tags
-
end
-
-
node_or_tags = coerce(node_or_tags)
-
-
if node_or_tags.is_a?(XML::NodeSet)
-
node_or_tags.each { |n| add_previous_sibling n }
-
unlink
-
else
-
replace_node node_or_tags
-
end
-
node_or_tags
-
end
-
-
####
-
# Swap this Node for +node_or_tags+
-
# +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup.
-
#
-
# Returns self, to support chaining of calls.
-
#
-
# Also see related method +replace+.
-
1
def swap node_or_tags
-
replace node_or_tags
-
self
-
end
-
-
1
alias :next :next_sibling
-
1
alias :previous :previous_sibling
-
-
# :stopdoc:
-
# HACK: This is to work around an RDoc bug
-
1
alias :next= :add_next_sibling
-
# :startdoc:
-
-
1
alias :previous= :add_previous_sibling
-
1
alias :remove :unlink
-
1
alias :get_attribute :[]
-
1
alias :attr :[]
-
1
alias :set_attribute :[]=
-
1
alias :text :content
-
1
alias :inner_text :content
-
1
alias :has_attribute? :key?
-
1
alias :name :node_name
-
1
alias :name= :node_name=
-
1
alias :type :node_type
-
1
alias :to_str :text
-
1
alias :clone :dup
-
1
alias :elements :element_children
-
-
####
-
# Returns a hash containing the node's attributes. The key is
-
# the attribute name without any namespace, the value is a Nokogiri::XML::Attr
-
# representing the attribute.
-
# If you need to distinguish attributes with the same name, with different namespaces
-
# use #attribute_nodes instead.
-
1
def attributes
-
Hash[attribute_nodes.map { |node|
-
[node.node_name, node]
-
}]
-
end
-
-
###
-
# Get the attribute values for this Node.
-
1
def values
-
attribute_nodes.map(&:value)
-
end
-
-
###
-
# Get the attribute names for this Node.
-
1
def keys
-
attribute_nodes.map(&:node_name)
-
end
-
-
###
-
# Iterate over each attribute name and value pair for this Node.
-
1
def each
-
attribute_nodes.each { |node|
-
yield [node.node_name, node.value]
-
}
-
end
-
-
###
-
# Remove the attribute named +name+
-
1
def remove_attribute name
-
attr = attributes[name].remove if key? name
-
clear_xpath_context if Nokogiri.jruby?
-
attr
-
end
-
1
alias :delete :remove_attribute
-
-
###
-
# Returns true if this Node matches +selector+
-
1
def matches? selector
-
ancestors.last.search(selector).include?(self)
-
end
-
-
###
-
# Create a DocumentFragment containing +tags+ that is relative to _this_
-
# context node.
-
1
def fragment tags
-
type = document.html? ? Nokogiri::HTML : Nokogiri::XML
-
type::DocumentFragment.new(document, tags, self)
-
end
-
-
###
-
# Parse +string_or_io+ as a document fragment within the context of
-
# *this* node. Returns a XML::NodeSet containing the nodes parsed from
-
# +string_or_io+.
-
1
def parse string_or_io, options = nil
-
##
-
# When the current node is unparented and not an element node, use the
-
# document as the parsing context instead. Otherwise, the in-context
-
# parser cannot find an element or a document node.
-
# Document Fragments are also not usable by the in-context parser.
-
if !element? && !document? && (!parent || parent.fragment?)
-
return document.parse(string_or_io, options)
-
end
-
-
options ||= (document.html? ? ParseOptions::DEFAULT_HTML : ParseOptions::DEFAULT_XML)
-
if Integer === options
-
options = Nokogiri::XML::ParseOptions.new(options)
-
end
-
# Give the options to the user
-
yield options if block_given?
-
-
contents = string_or_io.respond_to?(:read) ?
-
string_or_io.read :
-
string_or_io
-
-
return Nokogiri::XML::NodeSet.new(document) if contents.empty?
-
-
##
-
# This is a horrible hack, but I don't care. See #313 for background.
-
error_count = document.errors.length
-
node_set = in_context(contents, options.to_i)
-
if node_set.empty? and document.errors.length > error_count and options.recover?
-
fragment = Nokogiri::HTML::DocumentFragment.parse contents
-
node_set = fragment.children
-
end
-
node_set
-
end
-
-
####
-
# Set the Node's content to a Text node containing +string+. The string gets XML escaped, not interpreted as markup.
-
1
def content= string
-
self.native_content = encode_special_chars(string.to_s)
-
end
-
-
###
-
# Set the parent Node for this Node
-
1
def parent= parent_node
-
parent_node.add_child(self)
-
parent_node
-
end
-
-
###
-
# Returns a Hash of {prefix => value} for all namespaces on this
-
# node and its ancestors.
-
#
-
# This method returns the same namespaces as #namespace_scopes.
-
#
-
# Returns namespaces in scope for self -- those defined on self
-
# element directly or any ancestor node -- as a Hash of
-
# attribute-name/value pairs. Note that the keys in this hash
-
# XML attributes that would be used to define this namespace,
-
# such as "xmlns:prefix", not just the prefix. Default namespace
-
# set on self will be included with key "xmlns". However,
-
# default namespaces set on ancestor will NOT be, even if self
-
# has no explicit default namespace.
-
1
def namespaces
-
Hash[namespace_scopes.map { |nd|
-
key = ['xmlns', nd.prefix].compact.join(':')
-
[key, nd.href]
-
}]
-
end
-
-
# Returns true if this is a Comment
-
1
def comment?
-
type == COMMENT_NODE
-
end
-
-
# Returns true if this is a CDATA
-
1
def cdata?
-
type == CDATA_SECTION_NODE
-
end
-
-
# Returns true if this is an XML::Document node
-
1
def xml?
-
type == DOCUMENT_NODE
-
end
-
-
# Returns true if this is an HTML::Document node
-
1
def html?
-
type == HTML_DOCUMENT_NODE
-
end
-
-
# Returns true if this is a Document
-
1
def document?
-
is_a? XML::Document
-
end
-
-
# Returns true if this is a ProcessingInstruction node
-
1
def processing_instruction?
-
type == PI_NODE
-
end
-
-
# Returns true if this is a Text node
-
1
def text?
-
type == TEXT_NODE
-
end
-
-
# Returns true if this is a DocumentFragment
-
1
def fragment?
-
type == DOCUMENT_FRAG_NODE
-
end
-
-
###
-
# Fetch the Nokogiri::HTML::ElementDescription for this node. Returns
-
# nil on XML documents and on unknown tags.
-
1
def description
-
return nil if document.xml?
-
Nokogiri::HTML::ElementDescription[name]
-
end
-
-
###
-
# Is this a read only node?
-
1
def read_only?
-
# According to gdome2, these are read-only node types
-
[NOTATION_NODE, ENTITY_NODE, ENTITY_DECL].include?(type)
-
end
-
-
# Returns true if this is an Element node
-
1
def element?
-
type == ELEMENT_NODE
-
end
-
1
alias :elem? :element?
-
-
###
-
# Turn this node in to a string. If the document is HTML, this method
-
# returns html. If the document is XML, this method returns XML.
-
1
def to_s
-
document.xml? ? to_xml : to_html
-
end
-
-
# Get the inner_html for this node's Node#children
-
1
def inner_html *args
-
children.map { |x| x.to_html(*args) }.join
-
end
-
-
# Get the path to this node as a CSS expression
-
1
def css_path
-
path.split(/\//).map { |part|
-
part.length == 0 ? nil : part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
-
}.compact.join(' > ')
-
end
-
-
###
-
# Get a list of ancestor Node for this Node. If +selector+ is given,
-
# the ancestors must match +selector+
-
1
def ancestors selector = nil
-
return NodeSet.new(document) unless respond_to?(:parent)
-
return NodeSet.new(document) unless parent
-
-
parents = [parent]
-
-
while parents.last.respond_to?(:parent)
-
break unless ctx_parent = parents.last.parent
-
parents << ctx_parent
-
end
-
-
return NodeSet.new(document, parents) unless selector
-
-
root = parents.last
-
search_results = root.search(selector)
-
-
NodeSet.new(document, parents.find_all { |parent|
-
search_results.include?(parent)
-
})
-
end
-
-
###
-
# Adds a default namespace supplied as a string +url+ href, to self.
-
# The consequence is as an xmlns attribute with supplied argument were
-
# present in parsed XML. A default namespace set with this method will
-
# now show up in #attributes, but when this node is serialized to XML an
-
# "xmlns" attribute will appear. See also #namespace and #namespace=
-
1
def default_namespace= url
-
add_namespace_definition(nil, url)
-
end
-
1
alias :add_namespace :add_namespace_definition
-
-
###
-
# Set the default namespace on this node (as would be defined with an
-
# "xmlns=" attribute in XML source), as a Namespace object +ns+. Note that
-
# a Namespace added this way will NOT be serialized as an xmlns attribute
-
# for this node. You probably want #default_namespace= instead, or perhaps
-
# #add_namespace_definition with a nil prefix argument.
-
1
def namespace= ns
-
return set_namespace(ns) unless ns
-
-
unless Nokogiri::XML::Namespace === ns
-
raise TypeError, "#{ns.class} can't be coerced into Nokogiri::XML::Namespace"
-
end
-
if ns.document != document
-
raise ArgumentError, 'namespace must be declared on the same document'
-
end
-
-
set_namespace ns
-
end
-
-
####
-
# Yields self and all children to +block+ recursively.
-
1
def traverse &block
-
children.each{|j| j.traverse(&block) }
-
block.call(self)
-
end
-
-
###
-
# Accept a visitor. This method calls "visit" on +visitor+ with self.
-
1
def accept visitor
-
visitor.visit(self)
-
end
-
-
###
-
# Test to see if this Node is equal to +other+
-
1
def == other
-
return false unless other
-
return false unless other.respond_to?(:pointer_id)
-
pointer_id == other.pointer_id
-
end
-
-
###
-
# Serialize Node using +options+. Save options can also be set using a
-
# block. See SaveOptions.
-
#
-
# These two statements are equivalent:
-
#
-
# node.serialize(:encoding => 'UTF-8', :save_with => FORMAT | AS_XML)
-
#
-
# or
-
#
-
# node.serialize(:encoding => 'UTF-8') do |config|
-
# config.format.as_xml
-
# end
-
#
-
1
def serialize *args, &block
-
options = args.first.is_a?(Hash) ? args.shift : {
-
:encoding => args[0],
-
:save_with => args[1]
-
}
-
-
encoding = options[:encoding] || document.encoding
-
options[:encoding] = encoding
-
-
outstring = ""
-
if encoding && outstring.respond_to?(:force_encoding)
-
outstring.force_encoding(Encoding.find(encoding))
-
end
-
io = StringIO.new(outstring)
-
write_to io, options, &block
-
io.string
-
end
-
-
###
-
# Serialize this Node to HTML
-
#
-
# doc.to_html
-
#
-
# See Node#write_to for a list of +options+. For formatted output,
-
# use Node#to_xhtml instead.
-
1
def to_html options = {}
-
to_format SaveOptions::DEFAULT_HTML, options
-
end
-
-
###
-
# Serialize this Node to XML using +options+
-
#
-
# doc.to_xml(:indent => 5, :encoding => 'UTF-8')
-
#
-
# See Node#write_to for a list of +options+
-
1
def to_xml options = {}
-
options[:save_with] ||= SaveOptions::DEFAULT_XML
-
serialize(options)
-
end
-
-
###
-
# Serialize this Node to XHTML using +options+
-
#
-
# doc.to_xhtml(:indent => 5, :encoding => 'UTF-8')
-
#
-
# See Node#write_to for a list of +options+
-
1
def to_xhtml options = {}
-
to_format SaveOptions::DEFAULT_XHTML, options
-
end
-
-
###
-
# Write Node to +io+ with +options+. +options+ modify the output of
-
# this method. Valid options are:
-
#
-
# * +:encoding+ for changing the encoding
-
# * +:indent_text+ the indentation text, defaults to one space
-
# * +:indent+ the number of +:indent_text+ to use, defaults to 2
-
# * +:save_with+ a combination of SaveOptions constants.
-
#
-
# To save with UTF-8 indented twice:
-
#
-
# node.write_to(io, :encoding => 'UTF-8', :indent => 2)
-
#
-
# To save indented with two dashes:
-
#
-
# node.write_to(io, :indent_text => '-', :indent => 2
-
#
-
1
def write_to io, *options
-
options = options.first.is_a?(Hash) ? options.shift : {}
-
encoding = options[:encoding] || options[0]
-
if Nokogiri.jruby?
-
save_options = options[:save_with] || options[1]
-
indent_times = options[:indent] || 0
-
else
-
save_options = options[:save_with] || options[1] || SaveOptions::FORMAT
-
indent_times = options[:indent] || 2
-
end
-
indent_text = options[:indent_text] || ' '
-
-
config = SaveOptions.new(save_options.to_i)
-
yield config if block_given?
-
-
native_write_to(io, encoding, indent_text * indent_times, config.options)
-
end
-
-
###
-
# Write Node as HTML to +io+ with +options+
-
#
-
# See Node#write_to for a list of +options+
-
1
def write_html_to io, options = {}
-
write_format_to SaveOptions::DEFAULT_HTML, io, options
-
end
-
-
###
-
# Write Node as XHTML to +io+ with +options+
-
#
-
# See Node#write_to for a list of +options+
-
1
def write_xhtml_to io, options = {}
-
write_format_to SaveOptions::DEFAULT_XHTML, io, options
-
end
-
-
###
-
# Write Node as XML to +io+ with +options+
-
#
-
# doc.write_xml_to io, :encoding => 'UTF-8'
-
#
-
# See Node#write_to for a list of options
-
1
def write_xml_to io, options = {}
-
options[:save_with] ||= SaveOptions::DEFAULT_XML
-
write_to io, options
-
end
-
-
###
-
# Compare two Node objects with respect to their Document. Nodes from
-
# different documents cannot be compared.
-
1
def <=> other
-
return nil unless other.is_a?(Nokogiri::XML::Node)
-
return nil unless document == other.document
-
compare other
-
end
-
-
###
-
# Do xinclude substitution on the subtree below node. If given a block, a
-
# Nokogiri::XML::ParseOptions object initialized from +options+, will be
-
# passed to it, allowing more convenient modification of the parser options.
-
1
def do_xinclude options = XML::ParseOptions::DEFAULT_XML, &block
-
options = Nokogiri::XML::ParseOptions.new(options) if Integer === options
-
-
# give options to user
-
yield options if block_given?
-
-
# call c extension
-
process_xincludes(options.to_i)
-
end
-
-
1
def canonicalize(mode=XML::XML_C14N_1_0,inclusive_namespaces=nil,with_comments=false)
-
c14n_root = self
-
document.canonicalize(mode, inclusive_namespaces, with_comments) do |node, parent|
-
tn = node.is_a?(XML::Node) ? node : parent
-
tn == c14n_root || tn.ancestors.include?(c14n_root)
-
end
-
end
-
-
1
private
-
-
1
def add_sibling next_or_previous, node_or_tags
-
impl = (next_or_previous == :next) ? :add_next_sibling_node : :add_previous_sibling_node
-
iter = (next_or_previous == :next) ? :reverse_each : :each
-
-
node_or_tags = coerce node_or_tags
-
if node_or_tags.is_a?(XML::NodeSet)
-
if text?
-
pivot = Nokogiri::XML::Node.new 'dummy', document
-
send impl, pivot
-
else
-
pivot = self
-
end
-
node_or_tags.send(iter) { |n| pivot.send impl, n }
-
pivot.unlink if text?
-
else
-
send impl, node_or_tags
-
end
-
node_or_tags
-
end
-
-
1
def to_format save_option, options
-
# FIXME: this is a hack around broken libxml versions
-
return dump_html if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1]
-
-
options[:save_with] = save_option unless options[:save_with]
-
serialize(options)
-
end
-
-
1
def write_format_to save_option, io, options
-
# FIXME: this is a hack around broken libxml versions
-
return (io << dump_html) if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1]
-
-
options[:save_with] ||= save_option
-
write_to io, options
-
end
-
-
1
def inspect_attributes
-
[:name, :namespace, :attribute_nodes, :children]
-
end
-
-
1
def coerce data # :nodoc:
-
case data
-
when XML::NodeSet
-
return data
-
when XML::DocumentFragment
-
return data.children
-
when String
-
return fragment(data).children
-
when Document, XML::Attr
-
# unacceptable
-
when XML::Node
-
return data
-
end
-
-
raise ArgumentError, <<-EOERR
-
Requires a Node, NodeSet or String argument, and cannot accept a #{data.class}.
-
(You probably want to select a node from the Document with at() or search(), or create a new Node via Node.new().)
-
EOERR
-
end
-
-
1
def implied_xpath_contexts # :nodoc:
-
[".//"]
-
end
-
-
1
def add_child_node_and_reparent_attrs node # :nodoc:
-
add_child_node node
-
node.attribute_nodes.find_all { |a| a.name =~ /:/ }.each do |attr_node|
-
attr_node.remove
-
node[attr_node.name] = attr_node.value
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class Node
-
###
-
# Save options for serializing nodes
-
1
class SaveOptions
-
# Format serialized xml
-
1
FORMAT = 1
-
# Do not include declarations
-
1
NO_DECLARATION = 2
-
# Do not include empty tags
-
1
NO_EMPTY_TAGS = 4
-
# Do not save XHTML
-
1
NO_XHTML = 8
-
# Save as XHTML
-
1
AS_XHTML = 16
-
# Save as XML
-
1
AS_XML = 32
-
# Save as HTML
-
1
AS_HTML = 64
-
-
1
if Nokogiri.jruby?
-
# Save builder created document
-
AS_BUILDER = 128
-
# the default for XML documents
-
DEFAULT_XML = AS_XML # https://github.com/sparklemotion/nokogiri/issues/#issue/415
-
# the default for HTML document
-
DEFAULT_HTML = NO_DECLARATION | NO_EMPTY_TAGS | AS_HTML
-
else
-
# the default for XML documents
-
1
DEFAULT_XML = FORMAT | AS_XML
-
# the default for HTML document
-
1
DEFAULT_HTML = FORMAT | NO_DECLARATION | NO_EMPTY_TAGS | AS_HTML
-
end
-
# the default for XHTML document
-
1
DEFAULT_XHTML = FORMAT | NO_DECLARATION | NO_EMPTY_TAGS | AS_XHTML
-
-
# Integer representation of the SaveOptions
-
1
attr_reader :options
-
-
# Create a new SaveOptions object with +options+
-
1
def initialize options = 0; @options = options; end
-
-
1
constants.each do |constant|
-
10
class_eval %{
-
def #{constant.downcase}
-
@options |= #{constant}
-
self
-
end
-
-
def #{constant.downcase}?
-
#{constant} & @options == #{constant}
-
end
-
}
-
end
-
-
1
alias :to_i :options
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
####
-
# A NodeSet contains a list of Nokogiri::XML::Node objects. Typically
-
# a NodeSet is return as a result of searching a Document via
-
# Nokogiri::XML::Searchable#css or Nokogiri::XML::Searchable#xpath
-
1
class NodeSet
-
1
include Nokogiri::XML::Searchable
-
1
include Enumerable
-
-
# The Document this NodeSet is associated with
-
1
attr_accessor :document
-
-
# Create a NodeSet with +document+ defaulting to +list+
-
1
def initialize document, list = []
-
@document = document
-
document.decorate(self)
-
list.each { |x| self << x }
-
yield self if block_given?
-
end
-
-
###
-
# Get the first element of the NodeSet.
-
1
def first n = nil
-
return self[0] unless n
-
list = []
-
n.times { |i| list << self[i] }
-
list
-
end
-
-
###
-
# Get the last element of the NodeSet.
-
1
def last
-
self[-1]
-
end
-
-
###
-
# Is this NodeSet empty?
-
1
def empty?
-
length == 0
-
end
-
-
###
-
# Returns the index of the first node in self that is == to +node+. Returns nil if no match is found.
-
1
def index(node)
-
each_with_index { |member, j| return j if member == node }
-
nil
-
end
-
-
###
-
# Insert +datum+ before the first Node in this NodeSet
-
1
def before datum
-
first.before datum
-
end
-
-
###
-
# Insert +datum+ after the last Node in this NodeSet
-
1
def after datum
-
last.after datum
-
end
-
-
1
alias :<< :push
-
1
alias :remove :unlink
-
-
###
-
# call-seq: css *rules, [namespace-bindings, custom-pseudo-class]
-
#
-
# Search this node set for CSS +rules+. +rules+ must be one or more CSS
-
# selectors. For example:
-
#
-
# For more information see Nokogiri::XML::Searchable#css
-
1
def css *args
-
rules, handler, ns, _ = extract_params(args)
-
-
inject(NodeSet.new(document)) do |set, node|
-
set += css_internal node, rules, handler, ns
-
end
-
end
-
-
###
-
# call-seq: xpath *paths, [namespace-bindings, variable-bindings, custom-handler-class]
-
#
-
# Search this node set for XPath +paths+. +paths+ must be one or more XPath
-
# queries.
-
#
-
# For more information see Nokogiri::XML::Searchable#xpath
-
1
def xpath *args
-
paths, handler, ns, binds = extract_params(args)
-
-
inject(NodeSet.new(document)) do |set, node|
-
set += node.xpath(*(paths + [ns, handler, binds].compact))
-
end
-
end
-
-
###
-
# Search this NodeSet's nodes' immediate children using CSS selector +selector+
-
1
def > selector
-
ns = document.root.namespaces
-
xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first
-
end
-
-
###
-
# call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class]
-
#
-
# Search this object for +paths+, and return only the first
-
# result. +paths+ must be one or more XPath or CSS queries.
-
#
-
# See Searchable#search for more information.
-
#
-
# Or, if passed an integer, index into the NodeSet:
-
#
-
# node_set.at(3) # same as node_set[3]
-
#
-
1
def at *args
-
if args.length == 1 && args.first.is_a?(Numeric)
-
return self[args.first]
-
end
-
-
super(*args)
-
end
-
1
alias :% :at
-
-
###
-
# Filter this list for nodes that match +expr+
-
1
def filter expr
-
find_all { |node| node.matches?(expr) }
-
end
-
-
###
-
# Append the class attribute +name+ to all Node objects in the NodeSet.
-
1
def add_class name
-
each do |el|
-
classes = el['class'].to_s.split(/\s+/)
-
el['class'] = classes.push(name).uniq.join " "
-
end
-
self
-
end
-
-
###
-
# Remove the class attribute +name+ from all Node objects in the NodeSet.
-
# If +name+ is nil, remove the class attribute from all Nodes in the
-
# NodeSet.
-
1
def remove_class name = nil
-
each do |el|
-
if name
-
classes = el['class'].to_s.split(/\s+/)
-
if classes.empty?
-
el.delete 'class'
-
else
-
el['class'] = (classes - [name]).uniq.join " "
-
end
-
else
-
el.delete "class"
-
end
-
end
-
self
-
end
-
-
###
-
# Set the attribute +key+ to +value+ or the return value of +blk+
-
# on all Node objects in the NodeSet.
-
1
def attr key, value = nil, &blk
-
unless Hash === key || key && (value || blk)
-
return first.attribute(key)
-
end
-
-
hash = key.is_a?(Hash) ? key : { key => value }
-
-
hash.each { |k,v| each { |el| el[k] = v || blk[el] } }
-
-
self
-
end
-
1
alias :set :attr
-
1
alias :attribute :attr
-
-
###
-
# Remove the attributed named +name+ from all Node objects in the NodeSet
-
1
def remove_attr name
-
each { |el| el.delete name }
-
self
-
end
-
-
###
-
# Iterate over each node, yielding to +block+
-
1
def each(&block)
-
0.upto(length - 1) do |x|
-
yield self[x]
-
end
-
end
-
-
###
-
# Get the inner text of all contained Node objects
-
#
-
# Note: This joins the text of all Node objects in the NodeSet:
-
#
-
# doc = Nokogiri::XML('<xml><a><d>foo</d><d>bar</d></a></xml>')
-
# doc.css('d').text # => "foobar"
-
#
-
# Instead, if you want to return the text of all nodes in the NodeSet:
-
#
-
# doc.css('d').map(&:text) # => ["foo", "bar"]
-
#
-
# See Nokogiri::XML::Node#content for more information.
-
1
def inner_text
-
collect(&:inner_text).join('')
-
end
-
1
alias :text :inner_text
-
-
###
-
# Get the inner html of all contained Node objects
-
1
def inner_html *args
-
collect{|j| j.inner_html(*args) }.join('')
-
end
-
-
###
-
# Wrap this NodeSet with +html+ or the results of the builder in +blk+
-
1
def wrap(html, &blk)
-
each do |j|
-
new_parent = document.parse(html).first
-
j.add_next_sibling(new_parent)
-
new_parent.add_child(j)
-
end
-
self
-
end
-
-
###
-
# Convert this NodeSet to a string.
-
1
def to_s
-
map(&:to_s).join
-
end
-
-
###
-
# Convert this NodeSet to HTML
-
1
def to_html *args
-
if Nokogiri.jruby?
-
options = args.first.is_a?(Hash) ? args.shift : {}
-
if !options[:save_with]
-
options[:save_with] = Node::SaveOptions::NO_DECLARATION | Node::SaveOptions::NO_EMPTY_TAGS | Node::SaveOptions::AS_HTML
-
end
-
args.insert(0, options)
-
end
-
map { |x| x.to_html(*args) }.join
-
end
-
-
###
-
# Convert this NodeSet to XHTML
-
1
def to_xhtml *args
-
map { |x| x.to_xhtml(*args) }.join
-
end
-
-
###
-
# Convert this NodeSet to XML
-
1
def to_xml *args
-
map { |x| x.to_xml(*args) }.join
-
end
-
-
1
alias :size :length
-
1
alias :to_ary :to_a
-
-
###
-
# Removes the last element from set and returns it, or +nil+ if
-
# the set is empty
-
1
def pop
-
return nil if length == 0
-
delete last
-
end
-
-
###
-
# Returns the first element of the NodeSet and removes it. Returns
-
# +nil+ if the set is empty.
-
1
def shift
-
return nil if length == 0
-
delete first
-
end
-
-
###
-
# Equality -- Two NodeSets are equal if the contain the same number
-
# of elements and if each element is equal to the corresponding
-
# element in the other NodeSet
-
1
def == other
-
return false unless other.is_a?(Nokogiri::XML::NodeSet)
-
return false unless length == other.length
-
each_with_index do |node, i|
-
return false unless node == other[i]
-
end
-
true
-
end
-
-
###
-
# Returns a new NodeSet containing all the children of all the nodes in
-
# the NodeSet
-
1
def children
-
inject(NodeSet.new(document)) { |set, node| set += node.children }
-
end
-
-
###
-
# Returns a new NodeSet containing all the nodes in the NodeSet
-
# in reverse order
-
1
def reverse
-
node_set = NodeSet.new(document)
-
(length - 1).downto(0) do |x|
-
node_set.push self[x]
-
end
-
node_set
-
end
-
-
###
-
# Return a nicely formated string representation
-
1
def inspect
-
"[#{map(&:inspect).join ', '}]"
-
end
-
-
1
alias :+ :|
-
-
1
private
-
-
1
def implied_xpath_contexts # :nodoc:
-
[".//", "self::"]
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class Notation < Struct.new(:name, :public_id, :system_id)
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# Parse options for passing to Nokogiri.XML or Nokogiri.HTML
-
#
-
# == Building combinations of parse options
-
# You can build your own combinations of these parse options by using any of the following methods:
-
# *Note*: All examples attempt to set the +RECOVER+ & +NOENT+ options. All examples use Ruby 2 optional parameter syntax.
-
# [Ruby's bitwise operators] You can use the Ruby bitwise operators to set various combinations.
-
# Nokogiri.XML('<content>Chapter 1</content', options: Nokogiri::XML::ParseOptions.new((1 << 0) | (1 << 1)))
-
# [Method chaining] Every option has an equivalent method in lowercase. You can chain these methods together to set various combinations.
-
# Nokogiri.XML('<content>Chapter 1</content', options: Nokogiri::XML::ParseOptions.new.recover.noent)
-
# [Using Ruby Blocks] You can also setup parse combinations in the block passed to Nokogiri.XML or Nokogiri.HTML
-
# Nokogiri.XML('<content>Chapter 1</content') {|config| config.recover.noent}
-
#
-
# == Removing particular parse options
-
# You can also remove options from an instance of +ParseOptions+ dynamically.
-
# Every option has an equivalent <code>no{option}</code> method in lowercase. You can call these methods on an instance of +ParseOptions+ to remove the option.
-
# Note that this is not available for +STRICT+.
-
#
-
# # Setting the RECOVER & NOENT options...
-
# options = Nokogiri::XML::ParseOptions.new.recover.noent
-
# # later...
-
# options.norecover # Removes the Nokogiri::XML::ParseOptions::RECOVER option
-
# options.nonoent # Removes the Nokogiri::XML::ParseOptions::NOENT option
-
#
-
1
class ParseOptions
-
# Strict parsing
-
1
STRICT = 0
-
# Recover from errors
-
1
RECOVER = 1 << 0
-
# Substitute entities
-
1
NOENT = 1 << 1
-
# Load external subsets
-
1
DTDLOAD = 1 << 2
-
# Default DTD attributes
-
1
DTDATTR = 1 << 3
-
# validate with the DTD
-
1
DTDVALID = 1 << 4
-
# suppress error reports
-
1
NOERROR = 1 << 5
-
# suppress warning reports
-
1
NOWARNING = 1 << 6
-
# pedantic error reporting
-
1
PEDANTIC = 1 << 7
-
# remove blank nodes
-
1
NOBLANKS = 1 << 8
-
# use the SAX1 interface internally
-
1
SAX1 = 1 << 9
-
# Implement XInclude substitution
-
1
XINCLUDE = 1 << 10
-
# Forbid network access. Recommended for dealing with untrusted documents.
-
1
NONET = 1 << 11
-
# Do not reuse the context dictionary
-
1
NODICT = 1 << 12
-
# remove redundant namespaces declarations
-
1
NSCLEAN = 1 << 13
-
# merge CDATA as text nodes
-
1
NOCDATA = 1 << 14
-
# do not generate XINCLUDE START/END nodes
-
1
NOXINCNODE = 1 << 15
-
# compact small text nodes; no modification of the tree allowed afterwards (will possibly crash if you try to modify the tree)
-
1
COMPACT = 1 << 16
-
# parse using XML-1.0 before update 5
-
1
OLD10 = 1 << 17
-
# do not fixup XINCLUDE xml:base uris
-
1
NOBASEFIX = 1 << 18
-
# relax any hardcoded limit from the parser
-
1
HUGE = 1 << 19
-
-
# the default options used for parsing XML documents
-
1
DEFAULT_XML = RECOVER | NONET
-
# the default options used for parsing HTML documents
-
1
DEFAULT_HTML = RECOVER | NOERROR | NOWARNING | NONET
-
-
1
attr_accessor :options
-
1
def initialize options = STRICT
-
@options = options
-
end
-
-
1
constants.each do |constant|
-
23
next if constant.to_sym == :STRICT
-
22
class_eval %{
-
def #{constant.downcase}
-
@options |= #{constant}
-
self
-
end
-
-
def no#{constant.downcase}
-
@options &= ~#{constant}
-
self
-
end
-
-
def #{constant.downcase}?
-
#{constant} & @options == #{constant}
-
end
-
}
-
end
-
-
1
def strict
-
@options &= ~RECOVER
-
self
-
end
-
-
1
def strict?
-
@options & RECOVER == STRICT
-
end
-
-
1
alias :to_i :options
-
-
1
def inspect
-
options = []
-
self.class.constants.each do |k|
-
options << k.downcase if send(:"#{k.downcase}?")
-
end
-
super.sub(/>$/, " " + options.join(', ') + ">")
-
end
-
end
-
end
-
end
-
1
require 'nokogiri/xml/pp/node'
-
1
require 'nokogiri/xml/pp/character_data'
-
1
module Nokogiri
-
1
module XML
-
1
module PP
-
1
module CharacterData
-
1
def pretty_print pp # :nodoc:
-
nice_name = self.class.name.split('::').last
-
pp.group(2, "#(#{nice_name} ", ')') do
-
pp.pp text
-
end
-
end
-
-
1
def inspect # :nodoc:
-
"#<#{self.class.name}:#{sprintf("0x%x",object_id)} #{text.inspect}>"
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
module PP
-
1
module Node
-
1
def inspect # :nodoc:
-
attributes = inspect_attributes.reject { |x|
-
begin
-
attribute = send x
-
!attribute || (attribute.respond_to?(:empty?) && attribute.empty?)
-
rescue NoMethodError
-
true
-
end
-
}.map { |attribute|
-
"#{attribute.to_s.sub(/_\w+/, 's')}=#{send(attribute).inspect}"
-
}.join ' '
-
"#<#{self.class.name}:#{sprintf("0x%x", object_id)} #{attributes}>"
-
end
-
-
1
def pretty_print pp # :nodoc:
-
nice_name = self.class.name.split('::').last
-
pp.group(2, "#(#{nice_name}:#{sprintf("0x%x", object_id)} {", '})') do
-
-
pp.breakable
-
attrs = inspect_attributes.map { |t|
-
[t, send(t)] if respond_to?(t)
-
}.compact.find_all { |x|
-
if x.last
-
if [:attribute_nodes, :children].include? x.first
-
!x.last.empty?
-
else
-
true
-
end
-
end
-
}
-
-
pp.seplist(attrs) do |v|
-
if [:attribute_nodes, :children].include? v.first
-
pp.group(2, "#{v.first.to_s.sub(/_\w+$/, 's')} = [", "]") do
-
pp.breakable
-
pp.seplist(v.last) do |item|
-
pp.pp item
-
end
-
end
-
else
-
pp.text "#{v.first} = "
-
pp.pp v.last
-
end
-
end
-
pp.breakable
-
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class ProcessingInstruction < Node
-
1
def initialize document, name, content
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# Nokogiri::XML::Reader parses an XML document similar to the way a cursor
-
# would move. The Reader is given an XML document, and yields nodes
-
# to an each block.
-
#
-
# Here is an example of usage:
-
#
-
# reader = Nokogiri::XML::Reader(<<-eoxml)
-
# <x xmlns:tenderlove='http://tenderlovemaking.com/'>
-
# <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
-
# </x>
-
# eoxml
-
#
-
# reader.each do |node|
-
#
-
# # node is an instance of Nokogiri::XML::Reader
-
# puts node.name
-
#
-
# end
-
#
-
# Note that Nokogiri::XML::Reader#each can only be called once!! Once
-
# the cursor moves through the entire document, you must parse the
-
# document again. So make sure that you capture any information you
-
# need during the first iteration.
-
#
-
# The Reader parser is good for when you need the speed of a SAX parser,
-
# but do not want to write a Document handler.
-
1
class Reader
-
1
include Enumerable
-
-
1
TYPE_NONE = 0
-
# Element node type
-
1
TYPE_ELEMENT = 1
-
# Attribute node type
-
1
TYPE_ATTRIBUTE = 2
-
# Text node type
-
1
TYPE_TEXT = 3
-
# CDATA node type
-
1
TYPE_CDATA = 4
-
# Entity Reference node type
-
1
TYPE_ENTITY_REFERENCE = 5
-
# Entity node type
-
1
TYPE_ENTITY = 6
-
# PI node type
-
1
TYPE_PROCESSING_INSTRUCTION = 7
-
# Comment node type
-
1
TYPE_COMMENT = 8
-
# Document node type
-
1
TYPE_DOCUMENT = 9
-
# Document Type node type
-
1
TYPE_DOCUMENT_TYPE = 10
-
# Document Fragment node type
-
1
TYPE_DOCUMENT_FRAGMENT = 11
-
# Notation node type
-
1
TYPE_NOTATION = 12
-
# Whitespace node type
-
1
TYPE_WHITESPACE = 13
-
# Significant Whitespace node type
-
1
TYPE_SIGNIFICANT_WHITESPACE = 14
-
# Element end node type
-
1
TYPE_END_ELEMENT = 15
-
# Entity end node type
-
1
TYPE_END_ENTITY = 16
-
# XML Declaration node type
-
1
TYPE_XML_DECLARATION = 17
-
-
# A list of errors encountered while parsing
-
1
attr_accessor :errors
-
-
# The encoding for the document
-
1
attr_reader :encoding
-
-
# The XML source
-
1
attr_reader :source
-
-
1
alias :self_closing? :empty_element?
-
-
1
def initialize source, url = nil, encoding = nil # :nodoc:
-
@source = source
-
@errors = []
-
@encoding = encoding
-
end
-
1
private :initialize
-
-
###
-
# Get a list of attributes for the current node.
-
1
def attributes
-
Hash[attribute_nodes.map { |node|
-
[node.name, node.to_s]
-
}].merge(namespaces || {})
-
end
-
-
###
-
# Get a list of attributes for the current node
-
1
def attribute_nodes
-
nodes = attr_nodes
-
nodes.each { |v| v.instance_variable_set(:@_r, self) }
-
nodes
-
end
-
-
###
-
# Move the cursor through the document yielding the cursor to the block
-
1
def each
-
while cursor = self.read
-
yield cursor
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class << self
-
###
-
# Create a new Nokogiri::XML::RelaxNG document from +string_or_io+.
-
# See Nokogiri::XML::RelaxNG for an example.
-
1
def RelaxNG string_or_io
-
RelaxNG.new(string_or_io)
-
end
-
end
-
-
###
-
# Nokogiri::XML::RelaxNG is used for validating XML against a
-
# RelaxNG schema.
-
#
-
# == Synopsis
-
#
-
# Validate an XML document against a RelaxNG schema. Loop over the errors
-
# that are returned and print them out:
-
#
-
# schema = Nokogiri::XML::RelaxNG(File.open(ADDRESS_SCHEMA_FILE))
-
# doc = Nokogiri::XML(File.open(ADDRESS_XML_FILE))
-
#
-
# schema.validate(doc).each do |error|
-
# puts error.message
-
# end
-
#
-
# The list of errors are Nokogiri::XML::SyntaxError objects.
-
1
class RelaxNG < Nokogiri::XML::Schema
-
end
-
end
-
end
-
1
require 'nokogiri/xml/sax/document'
-
1
require 'nokogiri/xml/sax/parser_context'
-
1
require 'nokogiri/xml/sax/parser'
-
1
require 'nokogiri/xml/sax/push_parser'
-
1
module Nokogiri
-
1
module XML
-
###
-
# SAX Parsers are event driven parsers. Nokogiri provides two different
-
# event based parsers when dealing with XML. If you want to do SAX style
-
# parsing using HTML, check out Nokogiri::HTML::SAX.
-
#
-
# The basic way a SAX style parser works is by creating a parser,
-
# telling the parser about the events we're interested in, then giving
-
# the parser some XML to process. The parser will notify you when
-
# it encounters events you said you would like to know about.
-
#
-
# To register for events, you simply subclass Nokogiri::XML::SAX::Document,
-
# and implement the methods for which you would like notification.
-
#
-
# For example, if I want to be notified when a document ends, and when an
-
# element starts, I would write a class like this:
-
#
-
# class MyDocument < Nokogiri::XML::SAX::Document
-
# def end_document
-
# puts "the document has ended"
-
# end
-
#
-
# def start_element name, attributes = []
-
# puts "#{name} started"
-
# end
-
# end
-
#
-
# Then I would instantiate a SAX parser with this document, and feed the
-
# parser some XML
-
#
-
# # Create a new parser
-
# parser = Nokogiri::XML::SAX::Parser.new(MyDocument.new)
-
#
-
# # Feed the parser some XML
-
# parser.parse(File.open(ARGV[0]))
-
#
-
# Now my document handler will be called when each node starts, and when
-
# then document ends. To see what kinds of events are available, take
-
# a look at Nokogiri::XML::SAX::Document.
-
#
-
# Two SAX parsers for XML are available, a parser that reads from a string
-
# or IO object as it feels necessary, and a parser that lets you spoon
-
# feed it XML. If you want to let Nokogiri deal with reading your XML,
-
# use the Nokogiri::XML::SAX::Parser. If you want to have fine grain
-
# control over the XML input, use the Nokogiri::XML::SAX::PushParser.
-
1
module SAX
-
###
-
# This class is used for registering types of events you are interested
-
# in handling. All of the methods on this class are available as
-
# possible events while parsing an XML document. To register for any
-
# particular event, just subclass this class and implement the methods
-
# you are interested in knowing about.
-
#
-
# To only be notified about start and end element events, write a class
-
# like this:
-
#
-
# class MyDocument < Nokogiri::XML::SAX::Document
-
# def start_element name, attrs = []
-
# puts "#{name} started!"
-
# end
-
#
-
# def end_element name
-
# puts "#{name} ended"
-
# end
-
# end
-
#
-
# You can use this event handler for any SAX style parser included with
-
# Nokogiri. See Nokogiri::XML::SAX, and Nokogiri::HTML::SAX.
-
1
class Document
-
###
-
# Called when an XML declaration is parsed
-
1
def xmldecl version, encoding, standalone
-
end
-
-
###
-
# Called when document starts parsing
-
1
def start_document
-
end
-
-
###
-
# Called when document ends parsing
-
1
def end_document
-
end
-
-
###
-
# Called at the beginning of an element
-
# * +name+ is the name of the tag
-
# * +attrs+ are an assoc list of namespaces and attributes, e.g.:
-
# [ ["xmlns:foo", "http://sample.net"], ["size", "large"] ]
-
1
def start_element name, attrs = []
-
end
-
-
###
-
# Called at the end of an element
-
# +name+ is the tag name
-
1
def end_element name
-
end
-
-
###
-
# Called at the beginning of an element
-
# +name+ is the element name
-
# +attrs+ is a list of attributes
-
# +prefix+ is the namespace prefix for the element
-
# +uri+ is the associated namespace URI
-
# +ns+ is a hash of namespace prefix:urls associated with the element
-
1
def start_element_namespace name, attrs = [], prefix = nil, uri = nil, ns = []
-
###
-
# Deal with SAX v1 interface
-
name = [prefix, name].compact.join(':')
-
attributes = ns.map { |ns_prefix,ns_uri|
-
[['xmlns', ns_prefix].compact.join(':'), ns_uri]
-
} + attrs.map { |attr|
-
[[attr.prefix, attr.localname].compact.join(':'), attr.value]
-
}
-
start_element name, attributes
-
end
-
-
###
-
# Called at the end of an element
-
# +name+ is the element's name
-
# +prefix+ is the namespace prefix associated with the element
-
# +uri+ is the associated namespace URI
-
1
def end_element_namespace name, prefix = nil, uri = nil
-
###
-
# Deal with SAX v1 interface
-
end_element [prefix, name].compact.join(':')
-
end
-
-
###
-
# Characters read between a tag. This method might be called multiple
-
# times given one contiguous string of characters.
-
#
-
# +string+ contains the character data
-
1
def characters string
-
end
-
-
###
-
# Called when comments are encountered
-
# +string+ contains the comment data
-
1
def comment string
-
end
-
-
###
-
# Called on document warnings
-
# +string+ contains the warning
-
1
def warning string
-
end
-
-
###
-
# Called on document errors
-
# +string+ contains the error
-
1
def error string
-
end
-
-
###
-
# Called when cdata blocks are found
-
# +string+ contains the cdata content
-
1
def cdata_block string
-
end
-
-
###
-
# Called when processing instructions are found
-
# +name+ is the target of the instruction
-
# +content+ is the value of the instruction
-
1
def processing_instruction name, content
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
module SAX
-
###
-
# This parser is a SAX style parser that reads it's input as it
-
# deems necessary. The parser takes a Nokogiri::XML::SAX::Document,
-
# an optional encoding, then given an XML input, sends messages to
-
# the Nokogiri::XML::SAX::Document.
-
#
-
# Here is an example of using this parser:
-
#
-
# # Create a subclass of Nokogiri::XML::SAX::Document and implement
-
# # the events we care about:
-
# class MyDoc < Nokogiri::XML::SAX::Document
-
# def start_element name, attrs = []
-
# puts "starting: #{name}"
-
# end
-
#
-
# def end_element name
-
# puts "ending: #{name}"
-
# end
-
# end
-
#
-
# # Create our parser
-
# parser = Nokogiri::XML::SAX::Parser.new(MyDoc.new)
-
#
-
# # Send some XML to the parser
-
# parser.parse(File.open(ARGV[0]))
-
#
-
# For more information about SAX parsers, see Nokogiri::XML::SAX. Also
-
# see Nokogiri::XML::SAX::Document for the available events.
-
1
class Parser
-
1
class Attribute < Struct.new(:localname, :prefix, :uri, :value)
-
end
-
-
# Encodinds this parser supports
-
1
ENCODINGS = {
-
'NONE' => 0, # No char encoding detected
-
'UTF-8' => 1, # UTF-8
-
'UTF16LE' => 2, # UTF-16 little endian
-
'UTF16BE' => 3, # UTF-16 big endian
-
'UCS4LE' => 4, # UCS-4 little endian
-
'UCS4BE' => 5, # UCS-4 big endian
-
'EBCDIC' => 6, # EBCDIC uh!
-
'UCS4-2143' => 7, # UCS-4 unusual ordering
-
'UCS4-3412' => 8, # UCS-4 unusual ordering
-
'UCS2' => 9, # UCS-2
-
'ISO-8859-1' => 10, # ISO-8859-1 ISO Latin 1
-
'ISO-8859-2' => 11, # ISO-8859-2 ISO Latin 2
-
'ISO-8859-3' => 12, # ISO-8859-3
-
'ISO-8859-4' => 13, # ISO-8859-4
-
'ISO-8859-5' => 14, # ISO-8859-5
-
'ISO-8859-6' => 15, # ISO-8859-6
-
'ISO-8859-7' => 16, # ISO-8859-7
-
'ISO-8859-8' => 17, # ISO-8859-8
-
'ISO-8859-9' => 18, # ISO-8859-9
-
'ISO-2022-JP' => 19, # ISO-2022-JP
-
'SHIFT-JIS' => 20, # Shift_JIS
-
'EUC-JP' => 21, # EUC-JP
-
'ASCII' => 22, # pure ASCII
-
}
-
-
# The Nokogiri::XML::SAX::Document where events will be sent.
-
1
attr_accessor :document
-
-
# The encoding beings used for this document.
-
1
attr_accessor :encoding
-
-
# Create a new Parser with +doc+ and +encoding+
-
1
def initialize doc = Nokogiri::XML::SAX::Document.new, encoding = 'UTF-8'
-
check_encoding(encoding)
-
@encoding = encoding
-
@document = doc
-
@warned = false
-
end
-
-
###
-
# Parse given +thing+ which may be a string containing xml, or an
-
# IO object.
-
1
def parse thing, &block
-
if thing.respond_to?(:read) && thing.respond_to?(:close)
-
parse_io(thing, &block)
-
else
-
parse_memory(thing, &block)
-
end
-
end
-
-
###
-
# Parse given +io+
-
1
def parse_io io, encoding = 'ASCII'
-
check_encoding(encoding)
-
@encoding = encoding
-
ctx = ParserContext.io(io, ENCODINGS[encoding])
-
yield ctx if block_given?
-
ctx.parse_with self
-
end
-
-
###
-
# Parse a file with +filename+
-
1
def parse_file filename
-
raise ArgumentError unless filename
-
raise Errno::ENOENT unless File.exist?(filename)
-
raise Errno::EISDIR if File.directory?(filename)
-
ctx = ParserContext.file filename
-
yield ctx if block_given?
-
ctx.parse_with self
-
end
-
-
1
def parse_memory data
-
ctx = ParserContext.memory data
-
yield ctx if block_given?
-
ctx.parse_with self
-
end
-
-
1
private
-
1
def check_encoding(encoding)
-
encoding.upcase!
-
raise ArgumentError.new("'#{encoding}' is not a valid encoding") unless ENCODINGS[encoding]
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
module SAX
-
###
-
# Context for XML SAX parsers. This class is usually not instantiated
-
# by the user. Instead, you should be looking at
-
# Nokogiri::XML::SAX::Parser
-
1
class ParserContext
-
1
def self.new thing, encoding = 'UTF-8'
-
[:read, :close].all? { |x| thing.respond_to?(x) } ?
-
io(thing, Parser::ENCODINGS[encoding]) : memory(thing)
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
module SAX
-
###
-
# PushParser can parse a document that is fed to it manually. It
-
# must be given a SAX::Document object which will be called with
-
# SAX events as the document is being parsed.
-
#
-
# Calling PushParser#<< writes XML to the parser, calling any SAX
-
# callbacks it can.
-
#
-
# PushParser#finish tells the parser that the document is finished
-
# and calls the end_document SAX method.
-
#
-
# Example:
-
#
-
# parser = PushParser.new(Class.new(XML::SAX::Document) {
-
# def start_document
-
# puts "start document called"
-
# end
-
# }.new)
-
# parser << "<div>hello<"
-
# parser << "/div>"
-
# parser.finish
-
1
class PushParser
-
-
# The Nokogiri::XML::SAX::Document on which the PushParser will be
-
# operating
-
1
attr_accessor :document
-
-
###
-
# Create a new PushParser with +doc+ as the SAX Document, providing
-
# an optional +file_name+ and +encoding+
-
1
def initialize(doc = XML::SAX::Document.new, file_name = nil, encoding = 'UTF-8')
-
@document = doc
-
@encoding = encoding
-
@sax_parser = XML::SAX::Parser.new(doc)
-
-
## Create our push parser context
-
initialize_native(@sax_parser, file_name)
-
end
-
-
###
-
# Write a +chunk+ of XML to the PushParser. Any callback methods
-
# that can be called will be called immediately.
-
1
def write chunk, last_chunk = false
-
native_write(chunk, last_chunk)
-
end
-
1
alias :<< :write
-
-
###
-
# Finish the parsing. This method is only necessary for
-
# Nokogiri::XML::SAX::Document#end_document to be called.
-
1
def finish
-
write '', true
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class << self
-
###
-
# Create a new Nokogiri::XML::Schema object using a +string_or_io+
-
# object.
-
1
def Schema string_or_io
-
Schema.new(string_or_io)
-
end
-
end
-
-
###
-
# Nokogiri::XML::Schema is used for validating XML against a schema
-
# (usually from an xsd file).
-
#
-
# == Synopsis
-
#
-
# Validate an XML document against a Schema. Loop over the errors that
-
# are returned and print them out:
-
#
-
# xsd = Nokogiri::XML::Schema(File.read(PO_SCHEMA_FILE))
-
# doc = Nokogiri::XML(File.read(PO_XML_FILE))
-
#
-
# xsd.validate(doc).each do |error|
-
# puts error.message
-
# end
-
#
-
# The list of errors are Nokogiri::XML::SyntaxError objects.
-
1
class Schema
-
# Errors while parsing the schema file
-
1
attr_accessor :errors
-
-
###
-
# Create a new Nokogiri::XML::Schema object using a +string_or_io+
-
# object.
-
1
def self.new string_or_io
-
from_document Nokogiri::XML(string_or_io)
-
end
-
-
###
-
# Validate +thing+ against this schema. +thing+ can be a
-
# Nokogiri::XML::Document object, or a filename. An Array of
-
# Nokogiri::XML::SyntaxError objects found while validating the
-
# +thing+ is returned.
-
1
def validate thing
-
if thing.is_a?(Nokogiri::XML::Document)
-
validate_document(thing)
-
elsif File.file?(thing)
-
validate_file(thing)
-
else
-
raise ArgumentError, "Must provide Nokogiri::Xml::Document or the name of an existing file"
-
end
-
end
-
-
###
-
# Returns true if +thing+ is a valid Nokogiri::XML::Document or
-
# file.
-
1
def valid? thing
-
validate(thing).length == 0
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
#
-
# The Searchable module declares the interface used for searching your DOM.
-
#
-
# It implements the public methods `search`, `css`, and `xpath`,
-
# as well as allowing specific implementations to specialize some
-
# of the important behaviors.
-
#
-
1
module Searchable
-
# Regular expression used by Searchable#search to determine if a query
-
# string is CSS or XPath
-
1
LOOKS_LIKE_XPATH = /^(\.\/|\/|\.\.|\.$)/
-
-
###
-
# call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class]
-
#
-
# Search this object for +paths+. +paths+ must be one or more XPath or CSS queries:
-
#
-
# node.search("div.employee", ".//title")
-
#
-
# A hash of namespace bindings may be appended:
-
#
-
# node.search('.//bike:tire', {'bike' => 'http://schwinn.com/'})
-
# node.search('bike|tire', {'bike' => 'http://schwinn.com/'})
-
#
-
# For XPath queries, a hash of variable bindings may also be
-
# appended to the namespace bindings. For example:
-
#
-
# node.search('.//address[@domestic=$value]', nil, {:value => 'Yes'})
-
#
-
# Custom XPath functions and CSS pseudo-selectors may also be
-
# defined. To define custom functions create a class and
-
# implement the function you want to define. The first argument
-
# to the method will be the current matching NodeSet. Any other
-
# arguments are ones that you pass in. Note that this class may
-
# appear anywhere in the argument list. For example:
-
#
-
# node.search('.//title[regex(., "\w+")]', 'div.employee:regex("[0-9]+")'
-
# Class.new {
-
# def regex node_set, regex
-
# node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ }
-
# end
-
# }.new
-
# )
-
#
-
# See Searchable#xpath and Searchable#css for further usage help.
-
1
def search *args
-
paths, handler, ns, binds = extract_params(args)
-
-
xpaths = paths.map(&:to_s).map do |path|
-
(path =~ LOOKS_LIKE_XPATH) ? path : xpath_query_from_css_rule(path, ns)
-
end.flatten.uniq
-
-
xpath(*(xpaths + [ns, handler, binds].compact))
-
end
-
1
alias :/ :search
-
-
###
-
# call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class]
-
#
-
# Search this object for +paths+, and return only the first
-
# result. +paths+ must be one or more XPath or CSS queries.
-
#
-
# See Searchable#search for more information.
-
1
def at *args
-
search(*args).first
-
end
-
1
alias :% :at
-
-
###
-
# call-seq: css *rules, [namespace-bindings, custom-pseudo-class]
-
#
-
# Search this object for CSS +rules+. +rules+ must be one or more CSS
-
# selectors. For example:
-
#
-
# node.css('title')
-
# node.css('body h1.bold')
-
# node.css('div + p.green', 'div#one')
-
#
-
# A hash of namespace bindings may be appended. For example:
-
#
-
# node.css('bike|tire', {'bike' => 'http://schwinn.com/'})
-
#
-
# Custom CSS pseudo classes may also be defined. To define
-
# custom pseudo classes, create a class and implement the custom
-
# pseudo class you want defined. The first argument to the
-
# method will be the current matching NodeSet. Any other
-
# arguments are ones that you pass in. For example:
-
#
-
# node.css('title:regex("\w+")', Class.new {
-
# def regex node_set, regex
-
# node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ }
-
# end
-
# }.new)
-
#
-
# Note that the CSS query string is case-sensitive with regards
-
# to your document type. That is, if you're looking for "H1" in
-
# an HTML document, you'll never find anything, since HTML tags
-
# will match only lowercase CSS queries. However, "H1" might be
-
# found in an XML document, where tags names are case-sensitive
-
# (e.g., "H1" is distinct from "h1").
-
#
-
1
def css *args
-
rules, handler, ns, _ = extract_params(args)
-
-
css_internal self, rules, handler, ns
-
end
-
-
##
-
# call-seq: css *rules, [namespace-bindings, custom-pseudo-class]
-
#
-
# Search this object for CSS +rules+, and return only the first
-
# match. +rules+ must be one or more CSS selectors.
-
#
-
# See Searchable#css for more information.
-
1
def at_css *args
-
css(*args).first
-
end
-
-
###
-
# call-seq: xpath *paths, [namespace-bindings, variable-bindings, custom-handler-class]
-
#
-
# Search this node for XPath +paths+. +paths+ must be one or more XPath
-
# queries.
-
#
-
# node.xpath('.//title')
-
#
-
# A hash of namespace bindings may be appended. For example:
-
#
-
# node.xpath('.//foo:name', {'foo' => 'http://example.org/'})
-
# node.xpath('.//xmlns:name', node.root.namespaces)
-
#
-
# A hash of variable bindings may also be appended to the namespace bindings. For example:
-
#
-
# node.xpath('.//address[@domestic=$value]', nil, {:value => 'Yes'})
-
#
-
# Custom XPath functions may also be defined. To define custom
-
# functions create a class and implement the function you want
-
# to define. The first argument to the method will be the
-
# current matching NodeSet. Any other arguments are ones that
-
# you pass in. Note that this class may appear anywhere in the
-
# argument list. For example:
-
#
-
# node.xpath('.//title[regex(., "\w+")]', Class.new {
-
# def regex node_set, regex
-
# node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ }
-
# end
-
# }.new)
-
#
-
1
def xpath *args
-
return NodeSet.new(document) unless document
-
-
paths, handler, ns, binds = extract_params(args)
-
-
sets = paths.map do |path|
-
ctx = XPathContext.new(self)
-
ctx.register_namespaces(ns)
-
path = path.gsub(/xmlns:/, ' :') unless Nokogiri.uses_libxml?
-
-
binds.each do |key,value|
-
ctx.register_variable key.to_s, value
-
end if binds
-
-
ctx.evaluate(path, handler)
-
end
-
return sets.first if sets.length == 1
-
-
NodeSet.new(document) do |combined|
-
sets.each do |set|
-
set.each do |node|
-
combined << node
-
end
-
end
-
end
-
end
-
-
##
-
# call-seq: xpath *paths, [namespace-bindings, variable-bindings, custom-handler-class]
-
#
-
# Search this node for XPath +paths+, and return only the first
-
# match. +paths+ must be one or more XPath queries.
-
#
-
# See Searchable#xpath for more information.
-
1
def at_xpath *args
-
xpath(*args).first
-
end
-
-
1
private
-
-
1
def css_internal node, rules, handler, ns
-
xpaths = rules.map { |rule| xpath_query_from_css_rule(rule, ns) }
-
node.xpath(*(xpaths + [ns, handler].compact))
-
end
-
-
1
def xpath_query_from_css_rule rule, ns
-
implied_xpath_contexts.map do |implied_xpath_context|
-
CSS.xpath_for(rule.to_s, :prefix => implied_xpath_context, :ns => ns)
-
end.join(' | ')
-
end
-
-
1
def extract_params params # :nodoc:
-
handler = params.find do |param|
-
![Hash, String, Symbol].include?(param.class)
-
end
-
params -= [handler] if handler
-
-
hashes = []
-
while Hash === params.last || params.last.nil?
-
hashes << params.pop
-
break if params.empty?
-
end
-
ns, binds = hashes.reverse
-
-
ns ||= document.root ? document.root.namespaces : {}
-
-
[params, handler, ns, binds]
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
###
-
# This class provides information about XML SyntaxErrors. These
-
# exceptions are typically stored on Nokogiri::XML::Document#errors.
-
1
class SyntaxError < ::Nokogiri::SyntaxError
-
1
attr_reader :domain
-
1
attr_reader :code
-
1
attr_reader :level
-
1
attr_reader :file
-
1
attr_reader :line
-
1
attr_reader :str1
-
1
attr_reader :str2
-
1
attr_reader :str3
-
1
attr_reader :int1
-
1
attr_reader :column
-
-
###
-
# return true if this is a non error
-
1
def none?
-
level == 0
-
end
-
-
###
-
# return true if this is a warning
-
1
def warning?
-
level == 1
-
end
-
-
###
-
# return true if this is an error
-
1
def error?
-
level == 2
-
end
-
-
###
-
# return true if this error is fatal
-
1
def fatal?
-
level == 3
-
end
-
-
1
def to_s
-
super.chomp
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class Text < Nokogiri::XML::CharacterData
-
1
def content=(string)
-
self.native_content = string.to_s
-
end
-
end
-
end
-
end
-
1
require 'nokogiri/xml/xpath/syntax_error'
-
-
1
module Nokogiri
-
1
module XML
-
1
class XPath
-
# The Nokogiri::XML::Document tied to this XPath instance
-
1
attr_accessor :document
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class XPath
-
1
class SyntaxError < XML::SyntaxError
-
1
def to_s
-
[super.chomp, str1].compact.join(': ')
-
end
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XML
-
1
class XPathContext
-
-
###
-
# Register namespaces in +namespaces+
-
1
def register_namespaces(namespaces)
-
namespaces.each do |k, v|
-
k = k.to_s.gsub(/.*:/,'') # strip off 'xmlns:' or 'xml:'
-
register_ns(k, v)
-
end
-
end
-
-
end
-
end
-
end
-
1
require 'nokogiri/xslt/stylesheet'
-
-
1
module Nokogiri
-
1
class << self
-
###
-
# Create a Nokogiri::XSLT::Stylesheet with +stylesheet+.
-
#
-
# Example:
-
#
-
# xslt = Nokogiri::XSLT(File.read(ARGV[0]))
-
#
-
1
def XSLT stylesheet, modules = {}
-
XSLT.parse(stylesheet, modules)
-
end
-
end
-
-
###
-
# See Nokogiri::XSLT::Stylesheet for creating and manipulating
-
# Stylesheet object.
-
1
module XSLT
-
1
class << self
-
###
-
# Parse the stylesheet in +string+, register any +modules+
-
1
def parse string, modules = {}
-
modules.each do |url, klass|
-
XSLT.register url, klass
-
end
-
-
if Nokogiri.jruby?
-
Stylesheet.parse_stylesheet_doc(XML.parse(string), string)
-
else
-
Stylesheet.parse_stylesheet_doc(XML.parse(string))
-
end
-
end
-
-
###
-
# Quote parameters in +params+ for stylesheet safety
-
1
def quote_params params
-
parray = (params.instance_of?(Hash) ? params.to_a.flatten : params).dup
-
parray.each_with_index do |v,i|
-
if i % 2 > 0
-
parray[i]=
-
if v =~ /'/
-
"concat('#{ v.gsub(/'/, %q{', "'", '}) }')"
-
else
-
"'#{v}'";
-
end
-
else
-
parray[i] = v.to_s
-
end
-
end
-
parray.flatten
-
end
-
end
-
end
-
end
-
1
module Nokogiri
-
1
module XSLT
-
###
-
# A Stylesheet represents an XSLT Stylesheet object. Stylesheet creation
-
# is done through Nokogiri.XSLT. Here is an example of transforming
-
# an XML::Document with a Stylesheet:
-
#
-
# doc = Nokogiri::XML(File.read('some_file.xml'))
-
# xslt = Nokogiri::XSLT(File.read('some_transformer.xslt'))
-
#
-
# puts xslt.transform(doc)
-
#
-
# See Nokogiri::XSLT::Stylesheet#transform for more transformation
-
# information.
-
1
class Stylesheet
-
###
-
# Apply an XSLT stylesheet to an XML::Document.
-
# +params+ is an array of strings used as XSLT parameters.
-
# returns serialized document
-
1
def apply_to document, params = []
-
serialize(transform(document, params))
-
end
-
end
-
end
-
end
-
1
if RUBY_VERSION < "1.9.2"
-
raise "This version of Capybara/Poltergeist does not support Ruby versions " \
-
"less than 1.9.2."
-
end
-
-
1
require 'capybara'
-
-
1
module Capybara
-
1
module Poltergeist
-
1
require 'capybara/poltergeist/utility'
-
1
require 'capybara/poltergeist/driver'
-
1
require 'capybara/poltergeist/browser'
-
1
require 'capybara/poltergeist/node'
-
1
require 'capybara/poltergeist/server'
-
1
require 'capybara/poltergeist/web_socket_server'
-
1
require 'capybara/poltergeist/client'
-
1
require 'capybara/poltergeist/inspector'
-
1
require 'capybara/poltergeist/network_traffic'
-
1
require 'capybara/poltergeist/errors'
-
1
require 'capybara/poltergeist/cookie'
-
end
-
end
-
-
1
Capybara.register_driver :poltergeist do |app|
-
1
Capybara::Poltergeist::Driver.new(app)
-
end
-
1
require "capybara/poltergeist/errors"
-
1
require "capybara/poltergeist/command"
-
1
require 'json'
-
1
require 'time'
-
-
1
module Capybara::Poltergeist
-
1
class Browser
-
1
ERROR_MAPPINGS = {
-
'Poltergeist.JavascriptError' => JavascriptError,
-
'Poltergeist.FrameNotFound' => FrameNotFound,
-
'Poltergeist.InvalidSelector' => InvalidSelector,
-
'Poltergeist.StatusFailError' => StatusFailError,
-
'Poltergeist.NoSuchWindowError' => NoSuchWindowError,
-
'Poltergeist.UnsupportedFeature' => UnsupportedFeature
-
}
-
-
1
attr_reader :server, :client, :logger
-
-
1
def initialize(server, client, logger = nil)
-
1
@server = server
-
1
@client = client
-
1
@logger = logger
-
end
-
-
1
def restart
-
server.restart
-
client.restart
-
-
self.debug = @debug if defined?(@debug)
-
self.js_errors = @js_errors if defined?(@js_errors)
-
self.extensions = @extensions if @extensions
-
end
-
-
1
def visit(url)
-
14
command 'visit', url
-
end
-
-
1
def current_url
-
1
command 'current_url'
-
end
-
-
1
def status_code
-
command 'status_code'
-
end
-
-
1
def body
-
command 'body'
-
end
-
-
1
def source
-
command 'source'
-
end
-
-
1
def title
-
command 'title'
-
end
-
-
1
def parents(page_id, id)
-
command 'parents', page_id, id
-
end
-
-
1
def find(method, selector)
-
79
result = command('find', method, selector)
-
157
result['ids'].map { |id| [result['page_id'], id] }
-
end
-
-
1
def find_within(page_id, id, method, selector)
-
command 'find_within', page_id, id, method, selector
-
end
-
-
1
def all_text(page_id, id)
-
command 'all_text', page_id, id
-
end
-
-
1
def visible_text(page_id, id)
-
2
command 'visible_text', page_id, id
-
end
-
-
1
def delete_text(page_id, id)
-
command 'delete_text', page_id, id
-
end
-
-
1
def property(page_id, id, name)
-
59
command 'property', page_id, id, name.to_s
-
end
-
-
1
def attributes(page_id, id)
-
command 'attributes', page_id, id
-
end
-
-
1
def attribute(page_id, id, name)
-
command 'attribute', page_id, id, name.to_s
-
end
-
-
1
def value(page_id, id)
-
command 'value', page_id, id
-
end
-
-
1
def set(page_id, id, value)
-
61
command 'set', page_id, id, value
-
end
-
-
1
def select_file(page_id, id, value)
-
command 'select_file', page_id, id, value
-
end
-
-
1
def tag_name(page_id, id)
-
61
command('tag_name', page_id, id).downcase
-
end
-
-
1
def visible?(page_id, id)
-
78
command 'visible', page_id, id
-
end
-
-
1
def disabled?(page_id, id)
-
76
command 'disabled', page_id, id
-
end
-
-
1
def click_coordinates(x, y)
-
command 'click_coordinates', x, y
-
end
-
-
1
def evaluate(script, *args)
-
command 'evaluate', script, *args
-
end
-
-
1
def execute(script, *args)
-
command 'execute', script, *args
-
end
-
-
1
def within_frame(handle, &block)
-
if handle.is_a?(Capybara::Node::Base)
-
command 'push_frame', [handle.native.page_id, handle.native.id]
-
else
-
command 'push_frame', handle
-
end
-
-
yield
-
ensure
-
command 'pop_frame'
-
end
-
-
1
def switch_to_frame(handle, &block)
-
case handle
-
when Capybara::Node::Base
-
command 'push_frame', [handle.native.page_id, handle.native.id]
-
when :parent
-
command 'pop_frame'
-
when :top
-
command 'pop_frame', true
-
end
-
end
-
-
1
def window_handle
-
command 'window_handle'
-
end
-
-
1
def window_handles
-
command 'window_handles'
-
end
-
-
1
def switch_to_window(handle)
-
command 'switch_to_window', handle
-
end
-
-
1
def open_new_window
-
command 'open_new_window'
-
end
-
-
1
def close_window(handle)
-
command 'close_window', handle
-
end
-
-
1
def find_window_handle(locator)
-
return locator if window_handles.include? locator
-
-
handle = command 'window_handle', locator
-
raise NoSuchWindowError unless handle
-
return handle
-
end
-
-
1
def within_window(locator, &block)
-
original = window_handle
-
handle = find_window_handle(locator)
-
switch_to_window(handle)
-
yield
-
ensure
-
switch_to_window(original)
-
end
-
-
1
def click(page_id, id)
-
15
command 'click', page_id, id
-
end
-
-
1
def right_click(page_id, id)
-
command 'right_click', page_id, id
-
end
-
-
1
def double_click(page_id, id)
-
command 'double_click', page_id, id
-
end
-
-
1
def hover(page_id, id)
-
command 'hover', page_id, id
-
end
-
-
1
def drag(page_id, id, other_id)
-
command 'drag', page_id, id, other_id
-
end
-
-
1
def drag_by(page_id, id, x, y)
-
command 'drag_by', page_id, id, x, y
-
end
-
-
1
def select(page_id, id, value)
-
command 'select', page_id, id, value
-
end
-
-
1
def trigger(page_id, id, event)
-
command 'trigger', page_id, id, event.to_s
-
end
-
-
1
def reset
-
10
command 'reset'
-
end
-
-
1
def scroll_to(left, top)
-
command 'scroll_to', left, top
-
end
-
-
1
def render(path, options = {})
-
check_render_options!(options)
-
options[:full] = !!options[:full]
-
command 'render', path.to_s, options
-
end
-
-
1
def render_base64(format, options = {})
-
check_render_options!(options)
-
options[:full] = !!options[:full]
-
command 'render_base64', format.to_s, options
-
end
-
-
1
def set_zoom_factor(zoom_factor)
-
command 'set_zoom_factor', zoom_factor
-
end
-
-
1
def set_paper_size(size)
-
command 'set_paper_size', size
-
end
-
-
1
def resize(width, height)
-
command 'resize', width, height
-
end
-
-
1
def send_keys(page_id, id, keys)
-
command 'send_keys', page_id, id, normalize_keys(keys)
-
end
-
-
1
def path(page_id, id)
-
command 'path', page_id, id
-
end
-
-
1
def network_traffic
-
command('network_traffic').values.map do |event|
-
NetworkTraffic::Request.new(
-
event['request'],
-
event['responseParts'].map { |response| NetworkTraffic::Response.new(response) },
-
event['error'] ? NetworkTraffic::Error.new(event['error']) : nil
-
)
-
end
-
end
-
-
1
def clear_network_traffic
-
command('clear_network_traffic')
-
end
-
-
1
def set_proxy(ip, port, type, user, password)
-
args = [ip, port, type]
-
args << user if user
-
args << password if password
-
command('set_proxy', *args)
-
end
-
-
1
def equals(page_id, id, other_id)
-
command('equals', page_id, id, other_id)
-
end
-
-
1
def get_headers
-
command 'get_headers'
-
end
-
-
1
def set_headers(headers)
-
command 'set_headers', headers
-
end
-
-
1
def add_headers(headers)
-
command 'add_headers', headers
-
end
-
-
1
def add_header(header, permanent)
-
command 'add_header', header, permanent
-
end
-
-
1
def response_headers
-
command 'response_headers'
-
end
-
-
1
def cookies
-
Hash[command('cookies').map { |cookie| [cookie['name'], Cookie.new(cookie)] }]
-
end
-
-
1
def set_cookie(cookie)
-
if cookie[:expires]
-
cookie[:expires] = cookie[:expires].to_i * 1000
-
end
-
-
command 'set_cookie', cookie
-
end
-
-
1
def remove_cookie(name)
-
command 'remove_cookie', name
-
end
-
-
1
def clear_cookies
-
command 'clear_cookies'
-
end
-
-
1
def cookies_enabled=(flag)
-
command 'cookies_enabled', !!flag
-
end
-
-
1
def set_http_auth(user, password)
-
command 'set_http_auth', user, password
-
end
-
-
1
def js_errors=(val)
-
@js_errors = val
-
command 'set_js_errors', !!val
-
end
-
-
1
def extensions=(names)
-
1
@extensions = names
-
1
Array(names).each do |name|
-
command 'add_extension', name
-
end
-
end
-
-
1
def url_whitelist=(whitelist)
-
command 'set_url_whitelist', *whitelist
-
end
-
-
1
def url_blacklist=(blacklist)
-
command 'set_url_blacklist', *blacklist
-
end
-
-
1
def debug=(val)
-
@debug = val
-
command 'set_debug', !!val
-
end
-
-
1
def clear_memory_cache
-
command 'clear_memory_cache'
-
end
-
-
1
def command(name, *args)
-
456
cmd = Command.new(name, *args)
-
456
log cmd.message
-
-
456
response = server.send(cmd)
-
456
log response
-
-
456
json = JSON.load(response)
-
-
456
if json['error']
-
klass = ERROR_MAPPINGS[json['error']['name']] || BrowserError
-
raise klass.new(json['error'])
-
else
-
456
json['response']
-
end
-
rescue DeadClient
-
restart
-
raise
-
end
-
-
1
def go_back
-
command 'go_back'
-
end
-
-
1
def go_forward
-
command 'go_forward'
-
end
-
-
1
def accept_confirm
-
command 'set_confirm_process', true
-
end
-
-
1
def dismiss_confirm
-
command 'set_confirm_process', false
-
end
-
-
#
-
# press "OK" with text (response) or default value
-
#
-
1
def accept_prompt(response)
-
command 'set_prompt_response', response || false
-
end
-
-
#
-
# press "Cancel"
-
#
-
1
def dismiss_prompt
-
command 'set_prompt_response', nil
-
end
-
-
1
def modal_message
-
command 'modal_message'
-
end
-
-
1
private
-
-
1
def log(message)
-
912
logger.puts message if logger
-
end
-
-
1
def check_render_options!(options)
-
if !!options[:full] && options.has_key?(:selector)
-
warn "Ignoring :selector in #render since :full => true was given at #{caller.first}"
-
options.delete(:selector)
-
end
-
end
-
-
1
KEY_ALIASES = {
-
command: :Meta,
-
equals: :Equal,
-
Control: :Ctrl,
-
control: :Ctrl,
-
mulitply: 'numpad*',
-
add: 'numpad+',
-
divide: 'numpad/',
-
subtract: 'numpad-',
-
decimal: 'numpad.'
-
}
-
-
1
def normalize_keys(keys)
-
keys.map do |key|
-
case key
-
when Array
-
# [:Shift, "s"] => { modifier: "shift", key: "S" }
-
# [:Shift, "string"] => { modifier: "shift", key: "STRING" }
-
# [:Ctrl, :Left] => { modifier: "ctrl", key: :Left }
-
# [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: :Left }
-
# [:Ctrl, :Left, :Left] => { modifier: "ctrl", key: [:Left, :Left] }
-
_keys = key.chunk {|k| k.is_a?(Symbol) && %w(shift ctrl control alt meta command).include?(k.to_s.downcase) }
-
modifiers = if _keys.peek[0]
-
_keys.next[1].map do |k|
-
k = k.to_s.downcase
-
k = 'ctrl' if k == 'control'
-
k = 'meta' if k == 'command'
-
k
-
end.join(',')
-
else
-
''
-
end
-
letter = normalize_keys(_keys.next[1].map {|k| k.is_a?(String) ? k.upcase : k })
-
{ modifier: modifiers, key: letter }
-
when Symbol
-
# Return a known sequence for PhantomJS
-
key = KEY_ALIASES.fetch(key, key)
-
if match = key.to_s.match(/numpad(.)/)
-
res = { key: match[1], modifier: 'keypad' }
-
elsif key !~ /^[A-Z]/
-
key = key.to_s.split('_').map{|e| e.capitalize}.join
-
end
-
res || { key: key }
-
when String
-
key # Plain string, nothing to do
-
end
-
end
-
end
-
end
-
end
-
1
require "timeout"
-
1
require "capybara/poltergeist/utility"
-
1
require 'cliver'
-
-
1
module Capybara::Poltergeist
-
1
class Client
-
1
PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
-
1
PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0']
-
1
PHANTOMJS_NAME = 'phantomjs'
-
-
1
KILL_TIMEOUT = 2 # seconds
-
-
1
def self.start(*args)
-
1
client = new(*args)
-
1
client.start
-
1
client
-
end
-
-
# Returns a proc, that when called will attempt to kill the given process.
-
# This is because implementing ObjectSpace.define_finalizer is tricky.
-
# Hat-Tip to @mperham for describing in detail:
-
# http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
-
1
def self.process_killer(pid)
-
1
proc do
-
begin
-
if Capybara::Poltergeist.windows?
-
Process.kill('KILL', pid)
-
else
-
Process.kill('TERM', pid)
-
begin
-
Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
-
rescue Timeout::Error
-
Process.kill('KILL', pid)
-
Process.wait(pid)
-
end
-
end
-
rescue Errno::ESRCH, Errno::ECHILD
-
# Zed's dead, baby
-
end
-
end
-
end
-
-
1
attr_reader :pid, :server, :path, :window_size, :phantomjs_options
-
-
1
def initialize(server, options = {})
-
1
@server = server
-
1
@path = Cliver::detect((options[:path] || PHANTOMJS_NAME), *['>=2.1.0', '< 3.0'])
-
@path ||= Cliver::detect!((options[:path] || PHANTOMJS_NAME), *PHANTOMJS_VERSION).tap do
-
warn "You're running an old version of PhantomJS, update to >= 2.1.1 for a better experience."
-
1
end
-
-
1
@window_size = options[:window_size] || [1024, 768]
-
1
@phantomjs_options = options[:phantomjs_options] || []
-
1
@phantomjs_logger = options[:phantomjs_logger] || $stdout
-
end
-
-
1
def start
-
1
@read_io, @write_io = IO.pipe
-
1
@out_thread = Thread.new {
-
1
while !@read_io.eof? && data = @read_io.readpartial(1024)
-
@phantomjs_logger.write(data)
-
end
-
}
-
-
1
process_options = {}
-
1
process_options[:pgroup] = true unless Capybara::Poltergeist.windows?
-
-
1
redirect_stdout do
-
1
@pid = Process.spawn(*command.map(&:to_s), process_options)
-
1
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
-
end
-
end
-
-
1
def stop
-
if pid
-
kill_phantomjs
-
@out_thread.kill
-
close_io
-
ObjectSpace.undefine_finalizer(self)
-
end
-
end
-
-
1
def restart
-
stop
-
start
-
end
-
-
1
def command
-
1
parts = [path]
-
1
parts.concat phantomjs_options
-
1
parts << PHANTOMJS_SCRIPT
-
1
parts << server.port
-
1
parts.concat window_size
-
1
parts
-
end
-
-
1
private
-
-
# This abomination is because JRuby doesn't support the :out option of
-
# Process.spawn. To be honest it works pretty bad with pipes too, because
-
# we ought close writing end in parent process immediately but JRuby will
-
# lose all the output from child. Process.popen can be used here and seems
-
# it works with JRuby but I've experienced strange mistakes on Rubinius.
-
1
def redirect_stdout
-
1
prev = STDOUT.dup
-
1
$stdout = @write_io
-
1
STDOUT.reopen(@write_io)
-
1
yield
-
ensure
-
1
STDOUT.reopen(prev)
-
1
$stdout = STDOUT
-
1
prev.close
-
end
-
-
1
def kill_phantomjs
-
self.class.process_killer(pid).call
-
@pid = nil
-
end
-
-
# We grab all the output from PhantomJS like console.log in another thread
-
# and when PhantomJS crashes we try to restart it. In order to do it we stop
-
# server and client and on JRuby see this error `IOError: Stream closed`.
-
# It happens because JRuby tries to close pipe and it is blocked on `eof?`
-
# or `readpartial` call. The error is raised in the related thread and it's
-
# not actually main thread but the thread that listens to the output. That's
-
# why if you put some debug code after `rescue IOError` it won't be shown.
-
# In fact the main thread will continue working after the error even if we
-
# don't use `rescue`. The first attempt to fix it was a try not to block on
-
# IO, but looks like similar issue appers after JRuby upgrade. Perhaps the
-
# only way to fix it is catching the exception what this method overall does.
-
1
def close_io
-
[@write_io, @read_io].each do |io|
-
begin
-
io.close unless io.closed?
-
rescue IOError
-
raise unless RUBY_ENGINE == 'jruby'
-
end
-
end
-
end
-
end
-
end
-
1
require 'securerandom'
-
-
1
module Capybara::Poltergeist
-
1
class Command
-
1
attr_reader :id
-
1
attr_reader :name
-
1
attr_accessor :args
-
-
1
def initialize(name, *args)
-
456
@id = SecureRandom.uuid
-
456
@name = name
-
456
@args = args
-
end
-
-
1
def message
-
912
JSON.dump({ 'id' => @id, 'name' => @name, 'args' => @args })
-
end
-
end
-
end
-
1
module Capybara::Poltergeist
-
1
class Cookie
-
1
def initialize(attributes)
-
@attributes = attributes
-
end
-
-
1
def name
-
@attributes['name']
-
end
-
-
1
def value
-
@attributes['value']
-
end
-
-
1
def domain
-
@attributes['domain']
-
end
-
-
1
def path
-
@attributes['path']
-
end
-
-
1
def secure?
-
@attributes['secure']
-
end
-
-
1
def httponly?
-
@attributes['httponly']
-
end
-
-
1
def samesite
-
@attributes['samesite']
-
end
-
-
1
def expires
-
Time.at @attributes['expiry'] if @attributes['expiry']
-
end
-
end
-
end
-
1
require 'uri'
-
-
1
module Capybara::Poltergeist
-
1
class Driver < Capybara::Driver::Base
-
1
DEFAULT_TIMEOUT = 30
-
-
1
attr_reader :app, :options
-
-
1
def initialize(app, options = {})
-
1
@app = app
-
1
@options = options
-
1
@browser = nil
-
1
@inspector = nil
-
1
@server = nil
-
1
@client = nil
-
1
@started = false
-
end
-
-
1
def needs_server?
-
1
true
-
end
-
-
1
def browser
-
@browser ||= begin
-
1
browser = Browser.new(server, client, logger)
-
1
browser.js_errors = options[:js_errors] if options.key?(:js_errors)
-
1
browser.extensions = options.fetch(:extensions, [])
-
1
browser.debug = true if options[:debug]
-
1
browser.url_blacklist = options[:url_blacklist] if options.key?(:url_blacklist)
-
1
browser.url_whitelist = options[:url_whitelist] if options.key?(:url_whitelist)
-
1
browser
-
456
end
-
end
-
-
1
def inspector
-
1
@inspector ||= options[:inspector] && Inspector.new(options[:inspector])
-
end
-
-
1
def server
-
3
@server ||= Server.new(options[:port], options.fetch(:timeout) { DEFAULT_TIMEOUT })
-
end
-
-
1
def client
-
@client ||= Client.start(server,
-
:path => options[:phantomjs],
-
:window_size => options[:window_size],
-
:phantomjs_options => phantomjs_options,
-
:phantomjs_logger => phantomjs_logger
-
1
)
-
end
-
-
1
def phantomjs_options
-
1
list = options[:phantomjs_options] || []
-
-
# PhantomJS defaults to only using SSLv3, which since POODLE (Oct 2014)
-
# many sites have dropped from their supported protocols (eg PayPal,
-
# Braintree).
-
1
list += ["--ignore-ssl-errors=yes"] unless list.grep(/ignore-ssl-errors/).any?
-
1
list += ["--ssl-protocol=TLSv1"] unless list.grep(/ssl-protocol/).any?
-
1
list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
-
1
list
-
end
-
-
1
def client_pid
-
client.pid
-
end
-
-
1
def timeout
-
server.timeout
-
end
-
-
1
def timeout=(sec)
-
server.timeout = sec
-
end
-
-
1
def restart
-
browser.restart
-
end
-
-
1
def quit
-
server.stop
-
client.stop
-
end
-
-
# logger should be an object that responds to puts, or nil
-
1
def logger
-
1
options[:logger] || (options[:debug] && STDERR)
-
end
-
-
# logger should be an object that behaves like IO or nil
-
1
def phantomjs_logger
-
1
options.fetch(:phantomjs_logger, nil)
-
end
-
-
1
def visit(url)
-
14
@started = true
-
14
browser.visit(url)
-
end
-
-
1
def current_url
-
1
browser.current_url
-
end
-
-
1
def status_code
-
browser.status_code
-
end
-
-
1
def html
-
browser.body
-
end
-
1
alias_method :body, :html
-
-
1
def source
-
browser.source.to_s
-
end
-
-
1
def title
-
browser.title
-
end
-
-
1
def find(method, selector)
-
157
browser.find(method, selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
-
end
-
-
1
def find_xpath(selector)
-
79
find :xpath, selector
-
end
-
-
1
def find_css(selector)
-
find :css, selector
-
end
-
-
1
def click(x, y)
-
browser.click_coordinates(x, y)
-
end
-
-
1
def evaluate_script(script, *args)
-
browser.evaluate(script, *args.map { |arg| arg.is_a?(Capybara::Poltergeist::Node) ? arg.native : arg})
-
end
-
-
1
def execute_script(script, *args)
-
browser.execute(script, *args.map { |arg| arg.is_a?(Capybara::Poltergeist::Node) ? arg.native : arg})
-
nil
-
end
-
-
1
def within_frame(name, &block)
-
browser.within_frame(name, &block)
-
end
-
-
1
def switch_to_frame(locator, &block)
-
browser.switch_to_frame(locator, &block)
-
end
-
-
1
def current_window_handle
-
browser.window_handle
-
end
-
-
1
def window_handles
-
browser.window_handles
-
end
-
-
1
def close_window(handle)
-
browser.close_window(handle)
-
end
-
-
1
def open_new_window
-
browser.open_new_window
-
end
-
-
1
def switch_to_window(handle)
-
browser.switch_to_window(handle)
-
end
-
-
1
def within_window(name, &block)
-
browser.within_window(name, &block)
-
end
-
-
1
def no_such_window_error
-
NoSuchWindowError
-
end
-
-
1
def reset!
-
10
browser.reset
-
10
browser.url_blacklist = options[:url_blacklist] if options.key?(:url_blacklist)
-
10
browser.url_whitelist = options[:url_whitelist] if options.key?(:url_whitelist)
-
10
@started = false
-
end
-
-
1
def save_screenshot(path, options = {})
-
browser.render(path, options)
-
end
-
1
alias_method :render, :save_screenshot
-
-
1
def render_base64(format = :png, options = {})
-
browser.render_base64(format, options)
-
end
-
-
1
def paper_size=(size = {})
-
browser.set_paper_size(size)
-
end
-
-
1
def zoom_factor=(zoom_factor)
-
browser.set_zoom_factor(zoom_factor)
-
end
-
-
1
def resize(width, height)
-
browser.resize(width, height)
-
end
-
1
alias_method :resize_window, :resize
-
-
1
def resize_window_to(handle, width, height)
-
within_window(handle) do
-
resize(width, height)
-
end
-
end
-
-
1
def maximize_window(handle)
-
resize_window_to(handle, *screen_size)
-
end
-
-
1
def window_size(handle)
-
within_window(handle) do
-
evaluate_script('[window.innerWidth, window.innerHeight]')
-
end
-
end
-
-
1
def scroll_to(left, top)
-
browser.scroll_to(left, top)
-
end
-
-
1
def network_traffic
-
browser.network_traffic
-
end
-
-
1
def clear_network_traffic
-
browser.clear_network_traffic
-
end
-
-
1
def set_proxy(ip, port, type = "http", user = nil, password = nil)
-
browser.set_proxy(ip, port, type, user, password)
-
end
-
-
1
def headers
-
browser.get_headers
-
end
-
-
1
def headers=(headers)
-
browser.set_headers(headers)
-
end
-
-
1
def add_headers(headers)
-
browser.add_headers(headers)
-
end
-
-
1
def add_header(name, value, options = {})
-
permanent = options.fetch(:permanent, true)
-
browser.add_header({ name => value }, permanent)
-
end
-
-
1
def response_headers
-
browser.response_headers
-
end
-
-
1
def cookies
-
browser.cookies
-
end
-
-
1
def set_cookie(name, value, options = {})
-
options[:name] ||= name
-
options[:value] ||= value
-
options[:domain] ||= begin
-
if @started
-
URI.parse(browser.current_url).host
-
else
-
URI.parse(Capybara.app_host || '').host || "127.0.0.1"
-
end
-
end
-
-
browser.set_cookie(options)
-
end
-
-
1
def remove_cookie(name)
-
browser.remove_cookie(name)
-
end
-
-
1
def clear_cookies
-
browser.clear_cookies
-
end
-
-
1
def cookies_enabled=(flag)
-
browser.cookies_enabled = flag
-
end
-
-
1
def clear_memory_cache
-
browser.clear_memory_cache
-
end
-
-
# * PhantomJS with set settings doesn't send `Authorize` on POST request
-
# * With manually set header PhantomJS makes next request with
-
# `Authorization: Basic Og==` header when settings are empty and the
-
# response was `401 Unauthorized` (which means Base64.encode64(':')).
-
# Combining both methods to reach proper behavior.
-
1
def basic_authorize(user, password)
-
browser.set_http_auth(user, password)
-
credentials = ["#{user}:#{password}"].pack('m*').strip
-
add_header('Authorization', "Basic #{credentials}")
-
end
-
-
1
def debug
-
if @options[:inspector]
-
# Fall back to default scheme
-
scheme = URI.parse(browser.current_url).scheme rescue nil
-
scheme = 'http' if scheme != 'https'
-
inspector.open(scheme)
-
pause
-
else
-
raise Error, "To use the remote debugging, you have to launch the driver " \
-
"with `:inspector => true` configuration option"
-
end
-
end
-
-
1
def pause
-
# STDIN is not necessarily connected to a keyboard. It might even be closed.
-
# So we need a method other than keypress to continue.
-
-
# In jRuby - STDIN returns immediately from select
-
# see https://github.com/jruby/jruby/issues/1783
-
read, write = IO.pipe
-
Thread.new { IO.copy_stream(STDIN, write); write.close }
-
-
STDERR.puts "Poltergeist execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
-
-
signal = false
-
old_trap = trap('SIGCONT') { signal = true; STDERR.puts "\nSignal SIGCONT received" }
-
keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
-
-
begin
-
input = read.read_nonblock(80) # clear out the read buffer
-
puts unless input && input =~ /\n\z/
-
rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN.
-
end unless signal
-
-
trap('SIGCONT', old_trap) # Restore the previuos signal handler, if there was one.
-
-
STDERR.puts 'Continuing'
-
end
-
-
1
def wait?
-
true
-
end
-
-
1
def invalid_element_errors
-
[Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::MouseEventFailed]
-
end
-
-
1
def go_back
-
browser.go_back
-
end
-
-
1
def go_forward
-
browser.go_forward
-
end
-
-
1
def accept_modal(type, options = {})
-
case type
-
when :confirm
-
browser.accept_confirm
-
when :prompt
-
browser.accept_prompt options[:with]
-
end
-
-
yield if block_given?
-
-
find_modal(options)
-
end
-
-
1
def dismiss_modal(type, options = {})
-
case type
-
when :confirm
-
browser.dismiss_confirm
-
when :prompt
-
browser.dismiss_prompt
-
end
-
-
yield if block_given?
-
find_modal(options)
-
end
-
-
1
private
-
-
1
def screen_size
-
options[:screen_size] || [1366,768]
-
end
-
-
1
def find_modal(options)
-
start_time = Time.now
-
timeout_sec = options[:wait] || begin Capybara.default_max_wait_time rescue Capybara.default_wait_time end
-
expect_text = options[:text]
-
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
-
not_found_msg = 'Unable to find modal dialog'
-
not_found_msg += " with #{expect_text}" if expect_text
-
-
begin
-
modal_text = browser.modal_message
-
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
-
rescue Capybara::ModalNotFound => e
-
raise e, not_found_msg if (Time.now - start_time) >= timeout_sec
-
sleep(0.05)
-
retry
-
end
-
modal_text
-
end
-
end
-
end
-
1
module Capybara
-
1
module Poltergeist
-
1
class Error < StandardError; end
-
1
class NoSuchWindowError < Error; end
-
-
1
class ClientError < Error
-
1
attr_reader :response
-
-
1
def initialize(response)
-
@response = response
-
end
-
end
-
-
1
class JSErrorItem
-
1
attr_reader :message, :stack
-
-
1
def initialize(message, stack)
-
@message = message
-
@stack = stack
-
end
-
-
1
def to_s
-
[message, stack].join("\n")
-
end
-
end
-
-
1
class BrowserError < ClientError
-
1
def name
-
response['name']
-
end
-
-
1
def error_parameters
-
response['args'].join("\n")
-
end
-
-
1
def message
-
"There was an error inside the PhantomJS portion of Poltergeist. " \
-
"If this is the error returned, and not the cause of a more detailed error response, " \
-
"this is probably a bug, so please report it. " \
-
"\n\n#{name}: #{error_parameters}"
-
end
-
end
-
-
1
class JavascriptError < ClientError
-
1
def javascript_errors
-
response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
-
end
-
-
1
def message
-
"One or more errors were raised in the Javascript code on the page. " \
-
"If you don't care about these errors, you can ignore them by " \
-
"setting js_errors: false in your Poltergeist configuration (see " \
-
"documentation for details)." \
-
"\n\n#{javascript_errors.map(&:to_s).join("\n")}"
-
end
-
end
-
-
1
class StatusFailError < ClientError
-
1
def url
-
response['args'].first
-
end
-
-
1
def details
-
response['args'][1]
-
end
-
-
1
def message
-
msg = "Request to '#{url}' failed to reach server, check DNS and/or server status"
-
msg += " - #{details}" if details
-
msg
-
end
-
end
-
-
1
class FrameNotFound < ClientError
-
1
def name
-
response['args'].first
-
end
-
-
1
def message
-
"The frame '#{name}' was not found."
-
end
-
end
-
-
1
class InvalidSelector < ClientError
-
1
def method
-
response['args'][0]
-
end
-
-
1
def selector
-
response['args'][1]
-
end
-
-
1
def message
-
"The browser raised a syntax error while trying to evaluate " \
-
"#{method} selector #{selector.inspect}"
-
end
-
end
-
-
1
class NodeError < ClientError
-
1
attr_reader :node
-
-
1
def initialize(node, response)
-
@node = node
-
super(response)
-
end
-
end
-
-
1
class ObsoleteNode < NodeError
-
1
def message
-
"The element you are trying to interact with is either not part of the DOM, or is " \
-
"not currently visible on the page (perhaps display: none is set). " \
-
"It's possible the element has been replaced by another element and you meant to interact with " \
-
"the new element. If so you need to do a new 'find' in order to get a reference to the " \
-
"new element."
-
end
-
end
-
-
1
class UnsupportedFeature < ClientError
-
1
def name
-
response['name']
-
end
-
-
1
def unsupported_message
-
response['args'][0]
-
end
-
-
1
def version
-
response['args'][1].values_at(*%w(major minor patch)).join '.'
-
end
-
-
1
def message
-
"Running version of PhantomJS #{version} does not support some feature: #{unsupported_message}"
-
end
-
end
-
-
1
class MouseEventFailed < NodeError
-
1
def name
-
response['args'][0]
-
end
-
-
1
def selector
-
response['args'][1]
-
end
-
-
1
def position
-
[response['args'][2]['x'], response['args'][2]['y']]
-
end
-
-
1
def message
-
"Firing a #{name} at co-ordinates [#{position.join(', ')}] failed. Poltergeist detected " \
-
"another element with CSS selector '#{selector}' at this position. " \
-
"It may be overlapping the element you are trying to interact with. " \
-
"If you don't care about overlapping elements, try using node.trigger('#{name}')."
-
end
-
end
-
-
1
class TimeoutError < Error
-
1
def initialize(message)
-
@message = message
-
end
-
-
1
def message
-
"Timed out waiting for response to #{@message}. It's possible that this happened " \
-
"because something took a very long time (for example a page load was slow). " \
-
"If so, setting the Poltergeist :timeout option to a higher value will help " \
-
"(see the docs for details). If increasing the timeout does not help, this is " \
-
"probably a bug in Poltergeist - please report it to the issue tracker."
-
end
-
end
-
-
1
class DeadClient < Error
-
1
def initialize(message)
-
@message = message
-
end
-
-
1
def message
-
"PhantomJS client died while processing #{@message}"
-
end
-
end
-
-
1
class PhantomJSTooOld < Error
-
1
def self.===(other)
-
if Cliver::Dependency::VersionMismatch === other
-
warn "#{name} exception has been deprecated in favor of using the " +
-
"cliver gem for command-line dependency detection. Please " +
-
"handle Cliver::Dependency::VersionMismatch instead."
-
true
-
else
-
super
-
end
-
end
-
end
-
-
1
class PhantomJSFailed < Error
-
1
def self.===(other)
-
if Cliver::Dependency::NotMet === other
-
warn "#{name} exception has been deprecated in favor of using the " +
-
"cliver gem for command-line dependency detection. Please " +
-
"handle Cliver::Dependency::NotMet instead."
-
true
-
else
-
super
-
end
-
end
-
end
-
end
-
end
-
1
module Capybara::Poltergeist
-
1
class Inspector
-
1
BROWSERS = %w(chromium chromium-browser google-chrome open)
-
1
DEFAULT_PORT = 9664
-
-
1
def self.detect_browser
-
@browser ||= BROWSERS.find { |name| browser_binary_exists?(name) }
-
end
-
-
1
attr_reader :port
-
-
1
def initialize(browser = nil, port = DEFAULT_PORT)
-
@browser = browser.respond_to?(:to_str) ? browser : nil
-
@port = port
-
end
-
-
1
def browser
-
@browser ||= self.class.detect_browser
-
end
-
-
1
def url(scheme)
-
"#{scheme}://localhost:#{port}/"
-
end
-
-
1
def open(scheme)
-
if browser
-
Process.spawn(browser, url(scheme))
-
else
-
raise Error, "Could not find a browser executable to open #{url(scheme)}. " \
-
"You can specify one manually using e.g. `:inspector => 'chromium'` " \
-
"as a configuration option for Poltergeist."
-
end
-
end
-
-
1
def self.browser_binary_exists?(browser)
-
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
-
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
-
exts.each { |ext|
-
exe = "#{path}#{File::SEPARATOR}#{browser}#{ext}"
-
return exe if File.executable? exe
-
}
-
end
-
return nil
-
end
-
end
-
end
-
1
module Capybara::Poltergeist
-
1
module NetworkTraffic
-
1
require 'capybara/poltergeist/network_traffic/request'
-
1
require 'capybara/poltergeist/network_traffic/response'
-
1
require 'capybara/poltergeist/network_traffic/error'
-
end
-
end
-
1
module Capybara::Poltergeist::NetworkTraffic
-
1
class Error
-
1
def initialize(data)
-
@data = data
-
end
-
-
1
def url
-
@data['url']
-
end
-
-
1
def code
-
@data['errorCode']
-
end
-
-
1
def description
-
@data['errorString']
-
end
-
end
-
end
-
1
module Capybara::Poltergeist::NetworkTraffic
-
1
class Request
-
1
attr_reader :response_parts, :error
-
-
1
def initialize(data, response_parts = [], error = nil)
-
@data = data
-
@response_parts = response_parts
-
@error = error
-
end
-
-
1
def url
-
@data['url']
-
end
-
-
1
def method
-
@data['method']
-
end
-
-
1
def headers
-
@data['headers']
-
end
-
-
1
def time
-
@data['time'] && Time.parse(@data['time'])
-
end
-
end
-
end
-
1
module Capybara::Poltergeist::NetworkTraffic
-
1
class Response
-
1
def initialize(data)
-
@data = data
-
end
-
-
1
def url
-
@data['url']
-
end
-
-
1
def status
-
@data['status']
-
end
-
-
1
def status_text
-
@data['statusText']
-
end
-
-
1
def headers
-
@data['headers']
-
end
-
-
1
def redirect_url
-
@data['redirectURL']
-
end
-
-
1
def body_size
-
@data['bodySize']
-
end
-
-
1
def content_type
-
@data['contentType']
-
end
-
-
1
def time
-
@data['time'] && Time.parse(@data['time'])
-
end
-
end
-
end
-
-
1
module Capybara::Poltergeist
-
1
class Node < Capybara::Driver::Node
-
1
attr_reader :page_id, :id
-
-
1
def initialize(driver, page_id, id)
-
78
super(driver, self)
-
-
78
@page_id = page_id
-
78
@id = id
-
end
-
-
1
def browser
-
352
driver.browser
-
end
-
-
1
def command(name, *args)
-
352
browser.send(name, page_id, id, *args)
-
rescue BrowserError => error
-
case error.name
-
when 'Poltergeist.ObsoleteNode'
-
raise ObsoleteNode.new(self, error.response)
-
when 'Poltergeist.MouseEventFailed'
-
raise MouseEventFailed.new(self, error.response)
-
else
-
raise
-
end
-
end
-
-
1
def parents
-
command(:parents).map { |parent_id| self.class.new(driver, page_id, parent_id) }
-
end
-
-
1
def find(method, selector)
-
command(:find_within, method, selector).map { |id| self.class.new(driver, page_id, id) }
-
end
-
-
1
def find_xpath(selector)
-
find :xpath, selector
-
end
-
-
1
def find_css(selector)
-
find :css, selector
-
end
-
-
1
def all_text
-
filter_text command(:all_text)
-
end
-
-
1
def visible_text
-
2
filter_text command(:visible_text)
-
end
-
-
1
def property(name)
-
59
command :property, name
-
end
-
-
1
def [](name)
-
# Although the attribute matters, the property is consistent. Return that in
-
# preference to the attribute for links and images.
-
59
if (tag_name == 'img' and name == 'src') or (tag_name == 'a' and name == 'href' )
-
#if attribute exists get the property
-
value = command(:attribute, name) && command(:property, name)
-
return value
-
end
-
-
59
value = property(name)
-
59
value = command(:attribute, name) if value.nil? || value.is_a?(Hash)
-
-
59
value
-
end
-
-
1
def attributes
-
command :attributes
-
end
-
-
1
def value
-
command :value
-
end
-
-
1
def set(value)
-
61
if tag_name == 'input'
-
59
case self[:type]
-
when 'radio'
-
click
-
when 'checkbox'
-
click if value != checked?
-
when 'file'
-
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
-
command :select_file, files
-
else
-
59
command :set, value.to_s
-
end
-
2
elsif tag_name == 'textarea'
-
2
command :set, value.to_s
-
elsif self[:isContentEditable]
-
command :delete_text
-
send_keys(value.to_s)
-
end
-
end
-
-
1
def select_option
-
command :select, true
-
end
-
-
1
def unselect_option
-
command(:select, false) or
-
raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.")
-
end
-
-
1
def tag_name
-
181
@tag_name ||= command(:tag_name)
-
end
-
-
1
def visible?
-
78
command :visible?
-
end
-
-
1
def checked?
-
self[:checked]
-
end
-
-
1
def selected?
-
!!self[:selected]
-
end
-
-
1
def disabled?
-
76
command :disabled?
-
end
-
-
1
def click
-
15
command :click
-
end
-
-
1
def right_click
-
command :right_click
-
end
-
-
1
def double_click
-
command :double_click
-
end
-
-
1
def hover
-
command :hover
-
end
-
-
1
def drag_to(other)
-
command :drag, other.id
-
end
-
-
1
def drag_by(x, y)
-
command :drag_by, x, y
-
end
-
-
1
def trigger(event)
-
command :trigger, event
-
end
-
-
1
def ==(other)
-
command :equals, other.id
-
end
-
-
1
def send_keys(*keys)
-
command :send_keys, keys
-
end
-
1
alias_method :send_key, :send_keys
-
-
1
def path
-
command :path
-
end
-
-
# @api private
-
1
def to_json(*)
-
JSON.generate as_json
-
end
-
-
# @api private
-
1
def as_json(*)
-
{ ELEMENT: {page_id: @page_id, id: @id} }
-
end
-
-
1
private
-
-
1
def filter_text(text)
-
2
Capybara::Helpers.normalize_whitespace(text.to_s)
-
end
-
end
-
end
-
1
module Capybara::Poltergeist
-
1
class Server
-
1
attr_reader :socket, :fixed_port, :timeout
-
-
1
def initialize(fixed_port = nil, timeout = nil)
-
1
@fixed_port = fixed_port
-
1
@timeout = timeout
-
1
start
-
end
-
-
1
def port
-
1
@socket.port
-
end
-
-
1
def timeout=(sec)
-
@timeout = @socket.timeout = sec
-
end
-
-
1
def start
-
1
@socket = WebSocketServer.new(fixed_port, timeout)
-
end
-
-
1
def stop
-
@socket.close
-
end
-
-
1
def restart
-
stop
-
start
-
end
-
-
1
def send(command)
-
456
receive_timeout = nil # default
-
456
if command.name == 'visit'
-
14
command.args.push(timeout) # set the client set visit timeout parameter
-
14
receive_timeout = timeout + 5 # Add a couple of seconds to let the client timeout first
-
end
-
456
@socket.send(command.id, command.message, receive_timeout) or raise DeadClient.new(command.message)
-
end
-
end
-
end
-
1
module Capybara
-
1
module Poltergeist
-
1
class << self
-
1
def windows?
-
1
RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/
-
end
-
end
-
end
-
end
-
1
require 'socket'
-
1
require 'websocket/driver'
-
-
1
module Capybara::Poltergeist
-
# This is a 'custom' Web Socket server that is designed to be synchronous. What
-
# this means is that it sends a message, and then waits for a response. It does
-
# not expect to receive a message at any other time than right after it has sent
-
# a message. So it is basically operating a request/response cycle (which is not
-
# how Web Sockets are usually used, but it's what we want here, as we want to
-
# send a message to PhantomJS and then wait for it to respond).
-
1
class WebSocketServer
-
# How much to try to read from the socket at once (it's kinda arbitrary because we
-
# just keep reading until we've received a full frame)
-
1
RECV_SIZE = 1024
-
-
# How many seconds to try to bind to the port for before failing
-
1
BIND_TIMEOUT = 5
-
-
1
HOST = '127.0.0.1'
-
-
1
attr_reader :port, :driver, :socket, :server
-
1
attr_accessor :timeout
-
-
1
def initialize(port = nil, timeout = nil)
-
1
@timeout = timeout
-
1
@server = start_server(port)
-
1
@receive_mutex = Mutex.new
-
end
-
-
1
def start_server(port)
-
1
time = Time.now
-
-
1
begin
-
1
TCPServer.open(HOST, port || 0).tap do |server|
-
1
@port = server.addr[1]
-
end
-
rescue Errno::EADDRINUSE
-
if (Time.now - time) < BIND_TIMEOUT
-
sleep(0.01)
-
retry
-
else
-
raise
-
end
-
end
-
end
-
-
1
def connected?
-
456
!socket.nil?
-
end
-
-
# Accept a client on the TCP server socket, then receive its initial HTTP request
-
# and use that to initialize a Web Socket.
-
1
def accept
-
1
@socket = server.accept
-
1
@messages = {}
-
-
1
@driver = ::WebSocket::Driver.server(self)
-
2
@driver.on(:connect) { |event| @driver.start }
-
1
@driver.on(:message) do |event|
-
456
command_id = JSON.load(event.data)['command_id']
-
456
@messages[command_id] = event.data
-
end
-
end
-
-
1
def write(data)
-
457
@socket.write(data)
-
end
-
-
# Block until the next message is available from the Web Socket.
-
# Raises Errno::EWOULDBLOCK if timeout is reached.
-
1
def receive(cmd_id, receive_timeout=nil)
-
456
receive_timeout ||= timeout
-
456
start = Time.now
-
-
456
until @messages.has_key?(cmd_id)
-
457
raise Errno::EWOULDBLOCK if (Time.now - start) >= receive_timeout
-
457
if @receive_mutex.try_lock
-
457
begin
-
457
IO.select([socket], [], [], receive_timeout) or raise Errno::EWOULDBLOCK
-
457
data = socket.recv(RECV_SIZE)
-
457
break if data.empty?
-
457
driver.parse(data)
-
ensure
-
457
@receive_mutex.unlock
-
end
-
else
-
sleep(0.05)
-
end
-
end
-
456
@messages.delete(cmd_id)
-
end
-
-
# Send a message and block until there is a response
-
1
def send(cmd_id, message, accept_timeout=nil)
-
456
accept unless connected?
-
456
driver.text(message)
-
456
receive(cmd_id, accept_timeout)
-
rescue Errno::EWOULDBLOCK
-
raise TimeoutError.new(message)
-
end
-
-
# Closing sockets separately as `close_read`, `close_write`
-
# causes IO mistakes on JRuby, using just `close` fixes that.
-
1
def close
-
[server, socket].compact.each(&:close)
-
end
-
end
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
require "public_suffix/domain"
-
1
require "public_suffix/version"
-
1
require "public_suffix/errors"
-
1
require "public_suffix/rule"
-
1
require "public_suffix/list"
-
-
# PublicSuffix is a Ruby domain name parser based on the Public Suffix List.
-
#
-
# The [Public Suffix List](https://publicsuffix.org) is a cross-vendor initiative
-
# to provide an accurate list of domain name suffixes.
-
#
-
# The Public Suffix List is an initiative of the Mozilla Project,
-
# but is maintained as a community resource. It is available for use in any software,
-
# but was originally created to meet the needs of browser manufacturers.
-
1
module PublicSuffix
-
-
1
DOT = ".".freeze
-
1
BANG = "!".freeze
-
1
STAR = "*".freeze
-
-
# Parses +name+ and returns the {PublicSuffix::Domain} instance.
-
#
-
# @example Parse a valid domain
-
# PublicSuffix.parse("google.com")
-
# # => #<PublicSuffix::Domain ...>
-
#
-
# @example Parse a valid subdomain
-
# PublicSuffix.parse("www.google.com")
-
# # => #<PublicSuffix::Domain ...>
-
#
-
# @example Parse a fully qualified domain
-
# PublicSuffix.parse("google.com.")
-
# # => #<PublicSuffix::Domain ...>
-
#
-
# @example Parse a fully qualified domain (subdomain)
-
# PublicSuffix.parse("www.google.com.")
-
# # => #<PublicSuffix::Domain ...>
-
#
-
# @example Parse an invalid domain
-
# PublicSuffix.parse("x.yz")
-
# # => PublicSuffix::DomainInvalid
-
#
-
# @example Parse an URL (not supported, only domains)
-
# PublicSuffix.parse("http://www.google.com")
-
# # => PublicSuffix::DomainInvalid
-
#
-
#
-
# @param [String, #to_s] name The domain name or fully qualified domain name to parse.
-
# @param [PublicSuffix::List] list The rule list to search, defaults to the default {PublicSuffix::List}
-
# @param [Boolean] ignore_private
-
# @return [PublicSuffix::Domain]
-
#
-
# @raise [PublicSuffix::DomainInvalid]
-
# If domain is not a valid domain.
-
# @raise [PublicSuffix::DomainNotAllowed]
-
# If a rule for +domain+ is found, but the rule doesn't allow +domain+.
-
1
def self.parse(name, list: List.default, default_rule: list.default_rule, ignore_private: false)
-
what = normalize(name)
-
raise what if what.is_a?(DomainInvalid)
-
-
rule = list.find(what, default: default_rule, ignore_private: ignore_private)
-
-
# rubocop:disable Style/IfUnlessModifier
-
if rule.nil?
-
raise DomainInvalid, "`#{what}` is not a valid domain"
-
end
-
if rule.decompose(what).last.nil?
-
raise DomainNotAllowed, "`#{what}` is not allowed according to Registry policy"
-
end
-
# rubocop:enable Style/IfUnlessModifier
-
-
decompose(what, rule)
-
end
-
-
# Checks whether +domain+ is assigned and allowed, without actually parsing it.
-
#
-
# This method doesn't care whether domain is a domain or subdomain.
-
# The validation is performed using the default {PublicSuffix::List}.
-
#
-
# @example Validate a valid domain
-
# PublicSuffix.valid?("example.com")
-
# # => true
-
#
-
# @example Validate a valid subdomain
-
# PublicSuffix.valid?("www.example.com")
-
# # => true
-
#
-
# @example Validate a not-listed domain
-
# PublicSuffix.valid?("example.tldnotlisted")
-
# # => true
-
#
-
# @example Validate a not-allowed domain
-
# PublicSuffix.valid?("example.do")
-
# # => false
-
# PublicSuffix.valid?("www.example.do")
-
# # => true
-
#
-
# @example Validate a fully qualified domain
-
# PublicSuffix.valid?("google.com.")
-
# # => true
-
# PublicSuffix.valid?("www.google.com.")
-
# # => true
-
#
-
# @example Check an URL (which is not a valid domain)
-
# PublicSuffix.valid?("http://www.example.com")
-
# # => false
-
#
-
#
-
# @param [String, #to_s] name The domain name or fully qualified domain name to validate.
-
# @param [Boolean] ignore_private
-
# @return [Boolean]
-
1
def self.valid?(name, list: List.default, default_rule: list.default_rule, ignore_private: false)
-
what = normalize(name)
-
return false if what.is_a?(DomainInvalid)
-
-
rule = list.find(what, default: default_rule, ignore_private: ignore_private)
-
-
!rule.nil? && !rule.decompose(what).last.nil?
-
end
-
-
# Attempt to parse the name and returns the domain, if valid.
-
#
-
# This method doesn't raise. Instead, it returns nil if the domain is not valid for whatever reason.
-
#
-
# @param [String, #to_s] name The domain name or fully qualified domain name to parse.
-
# @param [PublicSuffix::List] list The rule list to search, defaults to the default {PublicSuffix::List}
-
# @param [Boolean] ignore_private
-
# @return [String]
-
1
def self.domain(name, **options)
-
parse(name, **options).domain
-
rescue PublicSuffix::Error
-
nil
-
end
-
-
-
# private
-
-
1
def self.decompose(name, rule)
-
left, right = rule.decompose(name)
-
-
parts = left.split(DOT)
-
# If we have 0 parts left, there is just a tld and no domain or subdomain
-
# If we have 1 part left, there is just a tld, domain and not subdomain
-
# If we have 2 parts left, the last part is the domain, the other parts (combined) are the subdomain
-
tld = right
-
sld = parts.empty? ? nil : parts.pop
-
trd = parts.empty? ? nil : parts.join(DOT)
-
-
Domain.new(tld, sld, trd)
-
end
-
-
# Pretend we know how to deal with user input.
-
1
def self.normalize(name)
-
name = name.to_s.dup
-
name.strip!
-
name.chomp!(DOT)
-
name.downcase!
-
-
return DomainInvalid.new("Name is blank") if name.empty?
-
return DomainInvalid.new("Name starts with a dot") if name.start_with?(DOT)
-
return DomainInvalid.new("%s is not expected to contain a scheme" % name) if name.include?("://")
-
name
-
end
-
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
module PublicSuffix
-
-
# Domain represents a domain name, composed by a TLD, SLD and TRD.
-
1
class Domain
-
-
# Splits a string into the labels, that is the dot-separated parts.
-
#
-
# The input is not validated, but it is assumed to be a valid domain name.
-
#
-
# @example
-
#
-
# name_to_labels('example.com')
-
# # => ['example', 'com']
-
#
-
# name_to_labels('example.co.uk')
-
# # => ['example', 'co', 'uk']
-
#
-
# @param name [String, #to_s] The domain name to split.
-
# @return [Array<String>]
-
1
def self.name_to_labels(name)
-
name.to_s.split(DOT)
-
end
-
-
-
1
attr_reader :tld, :sld, :trd
-
-
# Creates and returns a new {PublicSuffix::Domain} instance.
-
#
-
# @overload initialize(tld)
-
# Initializes with a +tld+.
-
# @param [String] tld The TLD (extension)
-
# @overload initialize(tld, sld)
-
# Initializes with a +tld+ and +sld+.
-
# @param [String] tld The TLD (extension)
-
# @param [String] sld The TRD (domain)
-
# @overload initialize(tld, sld, trd)
-
# Initializes with a +tld+, +sld+ and +trd+.
-
# @param [String] tld The TLD (extension)
-
# @param [String] sld The SLD (domain)
-
# @param [String] tld The TRD (subdomain)
-
#
-
# @yield [self] Yields on self.
-
# @yieldparam [PublicSuffix::Domain] self The newly creates instance
-
#
-
# @example Initialize with a TLD
-
# PublicSuffix::Domain.new("com")
-
# # => #<PublicSuffix::Domain @tld="com">
-
#
-
# @example Initialize with a TLD and SLD
-
# PublicSuffix::Domain.new("com", "example")
-
# # => #<PublicSuffix::Domain @tld="com", @trd=nil>
-
#
-
# @example Initialize with a TLD, SLD and TRD
-
# PublicSuffix::Domain.new("com", "example", "wwww")
-
# # => #<PublicSuffix::Domain @tld="com", @trd=nil, @sld="example">
-
#
-
1
def initialize(*args)
-
@tld, @sld, @trd = args
-
yield(self) if block_given?
-
end
-
-
# Returns a string representation of this object.
-
#
-
# @return [String]
-
1
def to_s
-
name
-
end
-
-
# Returns an array containing the domain parts.
-
#
-
# @return [Array<String, nil>]
-
#
-
# @example
-
#
-
# PublicSuffix::Domain.new("google.com").to_a
-
# # => [nil, "google", "com"]
-
#
-
# PublicSuffix::Domain.new("www.google.com").to_a
-
# # => [nil, "google", "com"]
-
#
-
1
def to_a
-
[@trd, @sld, @tld]
-
end
-
-
# Returns the full domain name.
-
#
-
# @return [String]
-
#
-
# @example Gets the domain name of a domain
-
# PublicSuffix::Domain.new("com", "google").name
-
# # => "google.com"
-
#
-
# @example Gets the domain name of a subdomain
-
# PublicSuffix::Domain.new("com", "google", "www").name
-
# # => "www.google.com"
-
#
-
1
def name
-
[@trd, @sld, @tld].compact.join(DOT)
-
end
-
-
# Returns a domain-like representation of this object
-
# if the object is a {#domain?}, <tt>nil</tt> otherwise.
-
#
-
# PublicSuffix::Domain.new("com").domain
-
# # => nil
-
#
-
# PublicSuffix::Domain.new("com", "google").domain
-
# # => "google.com"
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").domain
-
# # => "www.google.com"
-
#
-
# This method doesn't validate the input. It handles the domain
-
# as a valid domain name and simply applies the necessary transformations.
-
#
-
# This method returns a FQD, not just the domain part.
-
# To get the domain part, use <tt>#sld</tt> (aka second level domain).
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").domain
-
# # => "google.com"
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").sld
-
# # => "google"
-
#
-
# @see #domain?
-
# @see #subdomain
-
#
-
# @return [String]
-
1
def domain
-
[@sld, @tld].join(DOT) if domain?
-
end
-
-
# Returns a subdomain-like representation of this object
-
# if the object is a {#subdomain?}, <tt>nil</tt> otherwise.
-
#
-
# PublicSuffix::Domain.new("com").subdomain
-
# # => nil
-
#
-
# PublicSuffix::Domain.new("com", "google").subdomain
-
# # => nil
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").subdomain
-
# # => "www.google.com"
-
#
-
# This method doesn't validate the input. It handles the domain
-
# as a valid domain name and simply applies the necessary transformations.
-
#
-
# This method returns a FQD, not just the subdomain part.
-
# To get the subdomain part, use <tt>#trd</tt> (aka third level domain).
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").subdomain
-
# # => "www.google.com"
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").trd
-
# # => "www"
-
#
-
# @see #subdomain?
-
# @see #domain
-
#
-
# @return [String]
-
1
def subdomain
-
[@trd, @sld, @tld].join(DOT) if subdomain?
-
end
-
-
# Checks whether <tt>self</tt> looks like a domain.
-
#
-
# This method doesn't actually validate the domain.
-
# It only checks whether the instance contains
-
# a value for the {#tld} and {#sld} attributes.
-
# If you also want to validate the domain,
-
# use {#valid_domain?} instead.
-
#
-
# @example
-
#
-
# PublicSuffix::Domain.new("com").domain?
-
# # => false
-
#
-
# PublicSuffix::Domain.new("com", "google").domain?
-
# # => true
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").domain?
-
# # => true
-
#
-
# # This is an invalid domain, but returns true
-
# # because this method doesn't validate the content.
-
# PublicSuffix::Domain.new("com", nil).domain?
-
# # => true
-
#
-
# @see #subdomain?
-
#
-
# @return [Boolean]
-
1
def domain?
-
!(@tld.nil? || @sld.nil?)
-
end
-
-
# Checks whether <tt>self</tt> looks like a subdomain.
-
#
-
# This method doesn't actually validate the subdomain.
-
# It only checks whether the instance contains
-
# a value for the {#tld}, {#sld} and {#trd} attributes.
-
# If you also want to validate the domain,
-
# use {#valid_subdomain?} instead.
-
#
-
# @example
-
#
-
# PublicSuffix::Domain.new("com").subdomain?
-
# # => false
-
#
-
# PublicSuffix::Domain.new("com", "google").subdomain?
-
# # => false
-
#
-
# PublicSuffix::Domain.new("com", "google", "www").subdomain?
-
# # => true
-
#
-
# # This is an invalid domain, but returns true
-
# # because this method doesn't validate the content.
-
# PublicSuffix::Domain.new("com", "example", nil).subdomain?
-
# # => true
-
#
-
# @see #domain?
-
#
-
# @return [Boolean]
-
1
def subdomain?
-
!(@tld.nil? || @sld.nil? || @trd.nil?)
-
end
-
-
end
-
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
module PublicSuffix
-
-
1
class Error < StandardError
-
end
-
-
# Raised when trying to parse an invalid name.
-
# A name is considered invalid when no rule is found in the definition list.
-
#
-
# @example
-
#
-
# PublicSuffix.parse("nic.test")
-
# # => PublicSuffix::DomainInvalid
-
#
-
# PublicSuffix.parse("http://www.nic.it")
-
# # => PublicSuffix::DomainInvalid
-
#
-
1
class DomainInvalid < Error
-
end
-
-
# Raised when trying to parse a name that matches a suffix.
-
#
-
# @example
-
#
-
# PublicSuffix.parse("nic.do")
-
# # => PublicSuffix::DomainNotAllowed
-
#
-
# PublicSuffix.parse("www.nic.do")
-
# # => PublicSuffix::Domain
-
#
-
1
class DomainNotAllowed < DomainInvalid
-
end
-
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
module PublicSuffix
-
-
# A {PublicSuffix::List} is a collection of one
-
# or more {PublicSuffix::Rule}.
-
#
-
# Given a {PublicSuffix::List},
-
# you can add or remove {PublicSuffix::Rule},
-
# iterate all items in the list or search for the first rule
-
# which matches a specific domain name.
-
#
-
# # Create a new list
-
# list = PublicSuffix::List.new
-
#
-
# # Push two rules to the list
-
# list << PublicSuffix::Rule.factory("it")
-
# list << PublicSuffix::Rule.factory("com")
-
#
-
# # Get the size of the list
-
# list.size
-
# # => 2
-
#
-
# # Search for the rule matching given domain
-
# list.find("example.com")
-
# # => #<PublicSuffix::Rule::Normal>
-
# list.find("example.org")
-
# # => nil
-
#
-
# You can create as many {PublicSuffix::List} you want.
-
# The {PublicSuffix::List.default} rule list is used
-
# to tokenize and validate a domain.
-
#
-
# {PublicSuffix::List} implements +Enumerable+ module.
-
#
-
1
class List
-
1
include Enumerable
-
-
1
DEFAULT_LIST_PATH = File.join(File.dirname(__FILE__), "..", "..", "data", "list.txt")
-
-
# Gets the default rule list.
-
#
-
# Initializes a new {PublicSuffix::List} parsing the content
-
# of {PublicSuffix::List.default_list_content}, if required.
-
#
-
# @return [PublicSuffix::List]
-
1
def self.default(**options)
-
@default ||= parse(File.read(DEFAULT_LIST_PATH), options)
-
end
-
-
# Sets the default rule list to +value+.
-
#
-
# @param [PublicSuffix::List] value
-
# The new rule list.
-
#
-
# @return [PublicSuffix::List]
-
1
def self.default=(value)
-
@default = value
-
end
-
-
# Sets the default rule list to +nil+.
-
#
-
# @return [self]
-
1
def self.clear
-
self.default = nil
-
self
-
end
-
-
# rubocop:disable Metrics/MethodLength
-
-
# Parse given +input+ treating the content as Public Suffix List.
-
#
-
# See http://publicsuffix.org/format/ for more details about input format.
-
#
-
# @param string [#each_line] The list to parse.
-
# @param private_domain [Boolean] whether to ignore the private domains section.
-
# @return [Array<PublicSuffix::Rule::*>]
-
1
def self.parse(input, private_domains: true)
-
comment_token = "//".freeze
-
private_token = "===BEGIN PRIVATE DOMAINS===".freeze
-
section = nil # 1 == ICANN, 2 == PRIVATE
-
-
new do |list|
-
input.each_line do |line|
-
line.strip!
-
case # rubocop:disable Style/EmptyCaseCondition
-
-
# skip blank lines
-
when line.empty?
-
next
-
-
# include private domains or stop scanner
-
when line.include?(private_token)
-
break if !private_domains
-
section = 2
-
-
# skip comments
-
when line.start_with?(comment_token)
-
next
-
-
else
-
list.add(Rule.factory(line, private: section == 2), reindex: false)
-
-
end
-
end
-
end
-
end
-
# rubocop:enable Metrics/MethodLength
-
-
-
# Gets the array of rules.
-
#
-
# @return [Array<PublicSuffix::Rule::*>]
-
1
attr_reader :rules
-
-
-
# Initializes an empty {PublicSuffix::List}.
-
#
-
# @yield [self] Yields on self.
-
# @yieldparam [PublicSuffix::List] self The newly created instance.
-
#
-
1
def initialize
-
@rules = []
-
yield(self) if block_given?
-
reindex!
-
end
-
-
-
# Creates a naive index for +@rules+. Just a hash that will tell
-
# us where the elements of +@rules+ are relative to its first
-
# {PublicSuffix::Rule::Base#labels} element.
-
#
-
# For instance if @rules[5] and @rules[4] are the only elements of the list
-
# where Rule#labels.first is 'us' @indexes['us'] #=> [5,4], that way in
-
# select we can avoid mapping every single rule against the candidate domain.
-
1
def reindex!
-
@indexes = {}
-
@rules.each_with_index do |rule, index|
-
tld = Domain.name_to_labels(rule.value).last
-
@indexes[tld] ||= []
-
@indexes[tld] << index
-
end
-
end
-
-
# Gets the naive index, a hash that with the keys being the first label of
-
# every rule pointing to an array of integers (indexes of the rules in @rules).
-
1
def indexes
-
@indexes.dup
-
end
-
-
-
# Checks whether two lists are equal.
-
#
-
# List <tt>one</tt> is equal to <tt>two</tt>, if <tt>two</tt> is an instance of
-
# {PublicSuffix::List} and each +PublicSuffix::Rule::*+
-
# in list <tt>one</tt> is available in list <tt>two</tt>, in the same order.
-
#
-
# @param [PublicSuffix::List] other
-
# The List to compare.
-
#
-
# @return [Boolean]
-
1
def ==(other)
-
return false unless other.is_a?(List)
-
equal?(other) || rules == other.rules
-
end
-
1
alias eql? ==
-
-
# Iterates each rule in the list.
-
1
def each(*args, &block)
-
@rules.each(*args, &block)
-
end
-
-
-
# Adds the given object to the list and optionally refreshes the rule index.
-
#
-
# @param [PublicSuffix::Rule::*] rule
-
# The rule to add to the list.
-
# @param [Boolean] reindex
-
# Set to true to recreate the rule index
-
# after the rule has been added to the list.
-
#
-
# @return [self]
-
#
-
# @see #reindex!
-
#
-
1
def add(rule, reindex: true)
-
@rules << rule
-
reindex! if reindex
-
self
-
end
-
1
alias << add
-
-
# Gets the number of elements in the list.
-
#
-
# @return [Integer]
-
1
def size
-
@rules.size
-
end
-
-
# Checks whether the list is empty.
-
#
-
# @return [Boolean]
-
1
def empty?
-
@rules.empty?
-
end
-
-
# Removes all elements.
-
#
-
# @return [self]
-
1
def clear
-
@rules.clear
-
reindex!
-
self
-
end
-
-
# Finds and returns the most appropriate rule for the domain name.
-
#
-
# From the Public Suffix List documentation:
-
#
-
# - If a hostname matches more than one rule in the file,
-
# the longest matching rule (the one with the most levels) will be used.
-
# - An exclamation mark (!) at the start of a rule marks an exception to a previous wildcard rule.
-
# An exception rule takes priority over any other matching rule.
-
#
-
# ## Algorithm description
-
#
-
# 1. Match domain against all rules and take note of the matching ones.
-
# 2. If no rules match, the prevailing rule is "*".
-
# 3. If more than one rule matches, the prevailing rule is the one which is an exception rule.
-
# 4. If there is no matching exception rule, the prevailing rule is the one with the most labels.
-
# 5. If the prevailing rule is a exception rule, modify it by removing the leftmost label.
-
# 6. The public suffix is the set of labels from the domain
-
# which directly match the labels of the prevailing rule (joined by dots).
-
# 7. The registered domain is the public suffix plus one additional label.
-
#
-
# @param name [String, #to_s] The domain name.
-
# @param [PublicSuffix::Rule::*] default The default rule to return in case no rule matches.
-
# @return [PublicSuffix::Rule::*]
-
1
def find(name, default: default_rule, **options)
-
rule = select(name, **options).inject do |l, r|
-
return r if r.class == Rule::Exception
-
l.length > r.length ? l : r
-
end
-
rule || default
-
end
-
-
# Selects all the rules matching given domain.
-
#
-
# Internally, the lookup heavily rely on the `@indexes`. The input is split into labels,
-
# and we retriever from the index only the rules that end with the input label. After that,
-
# a sequential scan is performed. In most cases, where the number of rules for the same label
-
# is limited, this algorithm is efficient enough.
-
#
-
# If `ignore_private` is set to true, the algorithm will skip the rules that are flagged as private domain.
-
# Note that the rules will still be part of the loop. If you frequently need to access lists
-
# ignoring the private domains, you should create a list that doesn't include these domains setting the
-
# `private_domains: false` option when calling {.parse}.
-
#
-
# @param [String, #to_s] name The domain name.
-
# @param [Boolean] ignore_private
-
# @return [Array<PublicSuffix::Rule::*>]
-
1
def select(name, ignore_private: false)
-
name = name.to_s
-
indices = (@indexes[Domain.name_to_labels(name).last] || [])
-
-
finder = @rules.values_at(*indices).lazy
-
finder = finder.select { |rule| rule.match?(name) }
-
finder = finder.select { |rule| !rule.private } if ignore_private
-
finder.to_a
-
end
-
-
# Gets the default rule.
-
#
-
# @see PublicSuffix::Rule.default_rule
-
#
-
# @return [PublicSuffix::Rule::*]
-
1
def default_rule
-
PublicSuffix::Rule.default
-
end
-
-
end
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
module PublicSuffix
-
-
# A Rule is a special object which holds a single definition
-
# of the Public Suffix List.
-
#
-
# There are 3 types of rules, each one represented by a specific
-
# subclass within the +PublicSuffix::Rule+ namespace.
-
#
-
# To create a new Rule, use the {PublicSuffix::Rule#factory} method.
-
#
-
# PublicSuffix::Rule.factory("ar")
-
# # => #<PublicSuffix::Rule::Normal>
-
#
-
1
module Rule
-
-
# = Abstract rule class
-
#
-
# This represent the base class for a Rule definition
-
# in the {Public Suffix List}[https://publicsuffix.org].
-
#
-
# This is intended to be an Abstract class
-
# and you shouldn't create a direct instance. The only purpose
-
# of this class is to expose a common interface
-
# for all the available subclasses.
-
#
-
# * {PublicSuffix::Rule::Normal}
-
# * {PublicSuffix::Rule::Exception}
-
# * {PublicSuffix::Rule::Wildcard}
-
#
-
# ## Properties
-
#
-
# A rule is composed by 4 properties:
-
#
-
# value - A normalized version of the rule name.
-
# The normalization process depends on rule tpe.
-
#
-
# Here's an example
-
#
-
# PublicSuffix::Rule.factory("*.google.com")
-
# #<PublicSuffix::Rule::Wildcard:0x1015c14b0
-
# @value="google.com"
-
# >
-
#
-
# ## Rule Creation
-
#
-
# The best way to create a new rule is passing the rule name
-
# to the <tt>PublicSuffix::Rule.factory</tt> method.
-
#
-
# PublicSuffix::Rule.factory("com")
-
# # => PublicSuffix::Rule::Normal
-
#
-
# PublicSuffix::Rule.factory("*.com")
-
# # => PublicSuffix::Rule::Wildcard
-
#
-
# This method will detect the rule type and create an instance
-
# from the proper rule class.
-
#
-
# ## Rule Usage
-
#
-
# A rule describes the composition of a domain name and explains how to tokenize
-
# the name into tld, sld and trd.
-
#
-
# To use a rule, you first need to be sure the name you want to tokenize
-
# can be handled by the current rule.
-
# You can use the <tt>#match?</tt> method.
-
#
-
# rule = PublicSuffix::Rule.factory("com")
-
#
-
# rule.match?("google.com")
-
# # => true
-
#
-
# rule.match?("google.com")
-
# # => false
-
#
-
# Rule order is significant. A name can match more than one rule.
-
# See the {Public Suffix Documentation}[http://publicsuffix.org/format/]
-
# to learn more about rule priority.
-
#
-
# When you have the right rule, you can use it to tokenize the domain name.
-
#
-
# rule = PublicSuffix::Rule.factory("com")
-
#
-
# rule.decompose("google.com")
-
# # => ["google", "com"]
-
#
-
# rule.decompose("www.google.com")
-
# # => ["www.google", "com"]
-
#
-
# @abstract
-
#
-
1
class Base
-
-
# @return [String] the rule definition
-
1
attr_reader :value
-
-
# @return [Boolean] true if the rule is a private domain
-
1
attr_reader :private
-
-
-
# Initializes a new rule with name and value.
-
# If value is +nil+, name also becomes the value for this rule.
-
#
-
# @param value [String] the value of the rule
-
1
def initialize(value, private: false)
-
@value = value.to_s
-
@private = private
-
end
-
-
# Checks whether this rule is equal to <tt>other</tt>.
-
#
-
# @param [PublicSuffix::Rule::*] other The rule to compare
-
# @return [Boolean]
-
# Returns true if this rule and other are instances of the same class
-
# and has the same value, false otherwise.
-
1
def ==(other)
-
equal?(other) || (self.class == other.class && value == other.value)
-
end
-
1
alias eql? ==
-
-
# Checks if this rule matches +name+.
-
#
-
# A domain name is said to match a rule if and only if
-
# all of the following conditions are met:
-
#
-
# - When the domain and rule are split into corresponding labels,
-
# that the domain contains as many or more labels than the rule.
-
# - Beginning with the right-most labels of both the domain and the rule,
-
# and continuing for all labels in the rule, one finds that for every pair,
-
# either they are identical, or that the label from the rule is "*".
-
#
-
# @see https://publicsuffix.org/list/
-
#
-
# @example
-
# Rule.factory("com").match?("example.com")
-
# # => true
-
# Rule.factory("com").match?("example.net")
-
# # => false
-
#
-
# @param name [String, #to_s] The domain name to check.
-
# @return [Boolean]
-
1
def match?(name)
-
# Note: it works because of the assumption there are no
-
# rules like foo.*.com. If the assumption is incorrect,
-
# we need to properly walk the input and skip parts according
-
# to wildcard component.
-
diff = name.chomp(value)
-
diff.empty? || diff[-1] == "."
-
end
-
-
# @abstract
-
1
def parts
-
raise NotImplementedError
-
end
-
-
# @abstract
-
1
def length
-
raise NotImplementedError
-
end
-
-
# @abstract
-
# @param [String, #to_s] name The domain name to decompose
-
# @return [Array<String, nil>]
-
1
def decompose(*)
-
raise NotImplementedError
-
end
-
-
end
-
-
# Normal represents a standard rule (e.g. com).
-
1
class Normal < Base
-
-
# Gets the original rule definition.
-
#
-
# @return [String] The rule definition.
-
1
def rule
-
value
-
end
-
-
# Decomposes the domain name according to rule properties.
-
#
-
# @param [String, #to_s] name The domain name to decompose
-
# @return [Array<String>] The array with [trd + sld, tld].
-
1
def decompose(domain)
-
suffix = parts.join('\.')
-
matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
-
matches ? matches[1..2] : [nil, nil]
-
end
-
-
# dot-split rule value and returns all rule parts
-
# in the order they appear in the value.
-
#
-
# @return [Array<String>]
-
1
def parts
-
@value.split(DOT)
-
end
-
-
# Gets the length of this rule for comparison,
-
# represented by the number of dot-separated parts in the rule.
-
#
-
# @return [Integer] The length of the rule.
-
1
def length
-
@length ||= parts.length
-
end
-
-
end
-
-
# Wildcard represents a wildcard rule (e.g. *.co.uk).
-
1
class Wildcard < Base
-
-
# Initializes a new rule from +definition+.
-
#
-
# The wildcard "*" is removed from the value, as it's common
-
# for each wildcard rule.
-
#
-
# @param definition [String] the rule as defined in the PSL
-
1
def initialize(definition, private: false)
-
super(definition.to_s[2..-1], private: private)
-
end
-
-
# Gets the original rule definition.
-
#
-
# @return [String] The rule definition.
-
1
def rule
-
value == "" ? STAR : STAR + DOT + value
-
end
-
-
# Decomposes the domain name according to rule properties.
-
#
-
# @param [String, #to_s] name The domain name to decompose
-
# @return [Array<String>] The array with [trd + sld, tld].
-
1
def decompose(domain)
-
suffix = ([".*?"] + parts).join('\.')
-
matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
-
matches ? matches[1..2] : [nil, nil]
-
end
-
-
# dot-split rule value and returns all rule parts
-
# in the order they appear in the value.
-
#
-
# @return [Array<String>]
-
1
def parts
-
@value.split(DOT)
-
end
-
-
# Gets the length of this rule for comparison,
-
# represented by the number of dot-separated parts in the rule
-
# plus 1 for the *.
-
#
-
# @return [Integer] The length of the rule.
-
1
def length
-
@length ||= parts.length + 1 # * counts as 1
-
end
-
-
end
-
-
# Exception represents an exception rule (e.g. !parliament.uk).
-
1
class Exception < Base
-
-
# Initializes a new rule from +definition+.
-
#
-
# The bang ! is removed from the value, as it's common
-
# for each wildcard rule.
-
#
-
# @param definition [String] the rule as defined in the PSL
-
1
def initialize(definition, private: false)
-
super(definition.to_s[1..-1], private: private)
-
end
-
-
# Gets the original rule definition.
-
#
-
# @return [String] The rule definition.
-
1
def rule
-
BANG + value
-
end
-
-
# Decomposes the domain name according to rule properties.
-
#
-
# @param [String, #to_s] name The domain name to decompose
-
# @return [Array<String>] The array with [trd + sld, tld].
-
1
def decompose(domain)
-
suffix = parts.join('\.')
-
matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
-
matches ? matches[1..2] : [nil, nil]
-
end
-
-
# dot-split rule value and returns all rule parts
-
# in the order they appear in the value.
-
# The leftmost label is not considered a label.
-
#
-
# See http://publicsuffix.org/format/:
-
# If the prevailing rule is a exception rule,
-
# modify it by removing the leftmost label.
-
#
-
# @return [Array<String>]
-
1
def parts
-
@value.split(DOT)[1..-1]
-
end
-
-
# Gets the length of this rule for comparison,
-
# represented by the number of dot-separated parts in the rule.
-
#
-
# @return [Integer] The length of the rule.
-
1
def length
-
@length ||= parts.length
-
end
-
-
end
-
-
-
# Takes the +name+ of the rule, detects the specific rule class
-
# and creates a new instance of that class.
-
# The +name+ becomes the rule +value+.
-
#
-
# @example Creates a Normal rule
-
# PublicSuffix::Rule.factory("ar")
-
# # => #<PublicSuffix::Rule::Normal>
-
#
-
# @example Creates a Wildcard rule
-
# PublicSuffix::Rule.factory("*.ar")
-
# # => #<PublicSuffix::Rule::Wildcard>
-
#
-
# @example Creates an Exception rule
-
# PublicSuffix::Rule.factory("!congresodelalengua3.ar")
-
# # => #<PublicSuffix::Rule::Exception>
-
#
-
# @param [String] content The rule content.
-
# @return [PublicSuffix::Rule::*] A rule instance.
-
1
def self.factory(content, private: false)
-
case content.to_s[0, 1]
-
when STAR
-
Wildcard
-
when BANG
-
Exception
-
else
-
Normal
-
end.new(content, private: private)
-
end
-
-
# The default rule to use if no rule match.
-
#
-
# The default rule is "*". From https://publicsuffix.org/list/:
-
#
-
# > If no rules match, the prevailing rule is "*".
-
#
-
# @return [PublicSuffix::Rule::Wildcard] The default rule.
-
1
def self.default
-
factory(STAR)
-
end
-
-
end
-
-
end
-
# = Public Suffix
-
#
-
# Domain name parser based on the Public Suffix List.
-
#
-
# Copyright (c) 2009-2017 Simone Carletti <weppos@weppos.net>
-
-
1
module PublicSuffix
-
# The current library version.
-
1
VERSION = "2.0.5".freeze
-
end
-
# Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen>
-
#
-
# Rack is freely distributable under the terms of an MIT-style license.
-
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-
# The Rack main module, serving as a namespace for all core Rack
-
# modules and classes.
-
#
-
# All modules meant for use in your application are <tt>autoload</tt>ed here,
-
# so it should be enough just to <tt>require rack.rb</tt> in your code.
-
-
1
module Rack
-
# The Rack protocol version number implemented.
-
1
VERSION = [1,3]
-
-
# Return the Rack protocol version as a dotted string.
-
1
def self.version
-
VERSION.join(".")
-
end
-
-
# Return the Rack release as a dotted string.
-
1
def self.release
-
"1.6.5"
-
end
-
1
PATH_INFO = 'PATH_INFO'.freeze
-
1
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
-
1
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
-
1
QUERY_STRING = 'QUERY_STRING'.freeze
-
1
CACHE_CONTROL = 'Cache-Control'.freeze
-
1
CONTENT_LENGTH = 'Content-Length'.freeze
-
1
CONTENT_TYPE = 'Content-Type'.freeze
-
-
1
GET = 'GET'.freeze
-
1
HEAD = 'HEAD'.freeze
-
-
1
autoload :Builder, "rack/builder"
-
1
autoload :BodyProxy, "rack/body_proxy"
-
1
autoload :Cascade, "rack/cascade"
-
1
autoload :Chunked, "rack/chunked"
-
1
autoload :CommonLogger, "rack/commonlogger"
-
1
autoload :ConditionalGet, "rack/conditionalget"
-
1
autoload :Config, "rack/config"
-
1
autoload :ContentLength, "rack/content_length"
-
1
autoload :ContentType, "rack/content_type"
-
1
autoload :ETag, "rack/etag"
-
1
autoload :File, "rack/file"
-
1
autoload :Deflater, "rack/deflater"
-
1
autoload :Directory, "rack/directory"
-
1
autoload :ForwardRequest, "rack/recursive"
-
1
autoload :Handler, "rack/handler"
-
1
autoload :Head, "rack/head"
-
1
autoload :Lint, "rack/lint"
-
1
autoload :Lock, "rack/lock"
-
1
autoload :Logger, "rack/logger"
-
1
autoload :MethodOverride, "rack/methodoverride"
-
1
autoload :Mime, "rack/mime"
-
1
autoload :NullLogger, "rack/nulllogger"
-
1
autoload :Recursive, "rack/recursive"
-
1
autoload :Reloader, "rack/reloader"
-
1
autoload :Runtime, "rack/runtime"
-
1
autoload :Sendfile, "rack/sendfile"
-
1
autoload :Server, "rack/server"
-
1
autoload :ShowExceptions, "rack/showexceptions"
-
1
autoload :ShowStatus, "rack/showstatus"
-
1
autoload :Static, "rack/static"
-
1
autoload :TempfileReaper, "rack/tempfile_reaper"
-
1
autoload :URLMap, "rack/urlmap"
-
1
autoload :Utils, "rack/utils"
-
1
autoload :Multipart, "rack/multipart"
-
-
1
autoload :MockRequest, "rack/mock"
-
1
autoload :MockResponse, "rack/mock"
-
-
1
autoload :Request, "rack/request"
-
1
autoload :Response, "rack/response"
-
-
1
module Auth
-
1
autoload :Basic, "rack/auth/basic"
-
1
autoload :AbstractRequest, "rack/auth/abstract/request"
-
1
autoload :AbstractHandler, "rack/auth/abstract/handler"
-
1
module Digest
-
1
autoload :MD5, "rack/auth/digest/md5"
-
1
autoload :Nonce, "rack/auth/digest/nonce"
-
1
autoload :Params, "rack/auth/digest/params"
-
1
autoload :Request, "rack/auth/digest/request"
-
end
-
end
-
-
1
module Session
-
1
autoload :Cookie, "rack/session/cookie"
-
1
autoload :Pool, "rack/session/pool"
-
1
autoload :Memcache, "rack/session/memcache"
-
end
-
-
1
module Utils
-
1
autoload :OkJson, "rack/utils/okjson"
-
end
-
end
-
1
module Rack
-
1
class BodyProxy
-
1
def initialize(body, &block)
-
@body, @block, @closed = body, block, false
-
end
-
-
1
def respond_to?(*args)
-
return false if args.first.to_s =~ /^to_ary$/
-
super or @body.respond_to?(*args)
-
end
-
-
1
def close
-
return if @closed
-
@closed = true
-
begin
-
@body.close if @body.respond_to? :close
-
ensure
-
@block.call
-
end
-
end
-
-
1
def closed?
-
@closed
-
end
-
-
# N.B. This method is a special case to address the bug described by #434.
-
# We are applying this special case for #each only. Future bugs of this
-
# class will be handled by requesting users to patch their ruby
-
# implementation, to save adding too many methods in this class.
-
1
def each(*args, &block)
-
@body.each(*args, &block)
-
end
-
-
1
def method_missing(*args, &block)
-
super if args.first.to_s =~ /^to_ary$/
-
@body.__send__(*args, &block)
-
end
-
end
-
end
-
1
module Rack
-
# Rack::Builder implements a small DSL to iteratively construct Rack
-
# applications.
-
#
-
# Example:
-
#
-
# require 'rack/lobster'
-
# app = Rack::Builder.new do
-
# use Rack::CommonLogger
-
# use Rack::ShowExceptions
-
# map "/lobster" do
-
# use Rack::Lint
-
# run Rack::Lobster.new
-
# end
-
# end
-
#
-
# run app
-
#
-
# Or
-
#
-
# app = Rack::Builder.app do
-
# use Rack::CommonLogger
-
# run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
-
# end
-
#
-
# run app
-
#
-
# +use+ adds middleware to the stack, +run+ dispatches to an application.
-
# You can use +map+ to construct a Rack::URLMap in a convenient way.
-
-
1
class Builder
-
1
def self.parse_file(config, opts = Server::Options.new)
-
options = {}
-
if config =~ /\.ru$/
-
cfgfile = ::File.read(config)
-
if cfgfile[/^#\\(.*)/] && opts
-
options = opts.parse! $1.split(/\s+/)
-
end
-
cfgfile.sub!(/^__END__\n.*\Z/m, '')
-
app = new_from_string cfgfile, config
-
else
-
require config
-
app = Object.const_get(::File.basename(config, '.rb').capitalize)
-
end
-
return app, options
-
end
-
-
1
def self.new_from_string(builder_script, file="(rackup)")
-
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
-
TOPLEVEL_BINDING, file, 0
-
end
-
-
1
def initialize(default_app = nil,&block)
-
2
@use, @map, @run, @warmup = [], nil, default_app, nil
-
2
instance_eval(&block) if block_given?
-
end
-
-
1
def self.app(default_app = nil, &block)
-
self.new(default_app, &block).to_app
-
end
-
-
# Specifies middleware to use in a stack.
-
#
-
# class Middleware
-
# def initialize(app)
-
# @app = app
-
# end
-
#
-
# def call(env)
-
# env["rack.some_header"] = "setting an example"
-
# @app.call(env)
-
# end
-
# end
-
#
-
# use Middleware
-
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
-
#
-
# All requests through to this application will first be processed by the middleware class.
-
# The +call+ method in this example sets an additional environment key which then can be
-
# referenced in the application if required.
-
1
def use(middleware, *args, &block)
-
14
if @map
-
mapping, @map = @map, nil
-
@use << proc { |app| generate_map app, mapping }
-
end
-
28
@use << proc { |app| middleware.new(app, *args, &block) }
-
end
-
-
# Takes an argument that is an object that responds to #call and returns a Rack response.
-
# The simplest form of this is a lambda object:
-
#
-
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
-
#
-
# However this could also be a class:
-
#
-
# class Heartbeat
-
# def self.call(env)
-
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
-
# end
-
# end
-
#
-
# run Heartbeat
-
1
def run(app)
-
2
@run = app
-
end
-
-
# Takes a lambda or block that is used to warm-up the application.
-
#
-
# warmup do |app|
-
# client = Rack::MockRequest.new(app)
-
# client.get('/')
-
# end
-
#
-
# use SomeMiddleware
-
# run MyApp
-
1
def warmup(prc=nil, &block)
-
@warmup = prc || block
-
end
-
-
# Creates a route within the application.
-
#
-
# Rack::Builder.app do
-
# map '/' do
-
# run Heartbeat
-
# end
-
# end
-
#
-
# The +use+ method can also be used here to specify middleware to run under a specific path:
-
#
-
# Rack::Builder.app do
-
# map '/' do
-
# use Middleware
-
# run Heartbeat
-
# end
-
# end
-
#
-
# This example includes a piece of middleware which will run before requests hit +Heartbeat+.
-
#
-
1
def map(path, &block)
-
@map ||= {}
-
@map[path] = block
-
end
-
-
1
def to_app
-
2
app = @map ? generate_map(@run, @map) : @run
-
2
fail "missing run or map statement" unless app
-
16
app = @use.reverse.inject(app) { |a,e| e[a] }
-
2
@warmup.call(app) if @warmup
-
2
app
-
end
-
-
1
def call(env)
-
to_app.call(env)
-
end
-
-
1
private
-
-
1
def generate_map(default_app, mapping)
-
mapped = default_app ? {'/' => default_app} : {}
-
mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
-
URLMap.new(mapped)
-
end
-
end
-
end
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
# Rack::CommonLogger forwards every request to the given +app+, and
-
# logs a line in the
-
# {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
-
# to the +logger+.
-
#
-
# If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
-
# an instance of Rack::NullLogger.
-
#
-
# +logger+ can be any class, including the standard library Logger, and is
-
# expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT.
-
# According to the SPEC, the error stream must also respond to +puts+
-
# (which takes a single argument that responds to +to_s+), and +flush+
-
# (which is called without arguments in order to make the error appear for
-
# sure)
-
1
class CommonLogger
-
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
-
#
-
# lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
-
#
-
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
-
1
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
-
-
1
def initialize(app, logger=nil)
-
@app = app
-
@logger = logger
-
end
-
-
1
def call(env)
-
began_at = Time.now
-
status, header, body = @app.call(env)
-
header = Utils::HeaderHash.new(header)
-
body = BodyProxy.new(body) { log(env, status, header, began_at) }
-
[status, header, body]
-
end
-
-
1
private
-
-
1
def log(env, status, header, began_at)
-
now = Time.now
-
length = extract_content_length(header)
-
-
msg = FORMAT % [
-
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
-
env["REMOTE_USER"] || "-",
-
now.strftime("%d/%b/%Y:%H:%M:%S %z"),
-
env[REQUEST_METHOD],
-
env[PATH_INFO],
-
env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING],
-
env["HTTP_VERSION"],
-
status.to_s[0..3],
-
length,
-
now - began_at ]
-
-
logger = @logger || env['rack.errors']
-
# Standard library logger doesn't support write but it supports << which actually
-
# calls to write on the log device without formatting
-
if logger.respond_to?(:write)
-
logger.write(msg)
-
else
-
logger << msg
-
end
-
end
-
-
1
def extract_content_length(headers)
-
value = headers[CONTENT_LENGTH] or return '-'
-
value.to_s == '0' ? '-' : value
-
end
-
end
-
end
-
1
require 'rack/utils'
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
-
# Sets the Content-Length header on responses with fixed-length bodies.
-
1
class ContentLength
-
1
include Rack::Utils
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
1
def call(env)
-
status, headers, body = @app.call(env)
-
headers = HeaderHash.new(headers)
-
-
if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
-
!headers[CONTENT_LENGTH] &&
-
!headers['Transfer-Encoding'] &&
-
body.respond_to?(:to_ary)
-
-
obody = body
-
body, length = [], 0
-
obody.each { |part| body << part; length += bytesize(part) }
-
-
body = BodyProxy.new(body) do
-
obody.close if obody.respond_to?(:close)
-
end
-
-
headers[CONTENT_LENGTH] = length.to_s
-
end
-
-
[status, headers, body]
-
end
-
end
-
end
-
1
require 'time'
-
1
require 'rack/utils'
-
1
require 'rack/mime'
-
-
1
module Rack
-
# Rack::File serves files below the +root+ directory given, according to the
-
# path info of the Rack request.
-
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
-
# as http://localhost:9292/passwd
-
#
-
# Handlers can detect if bodies are a Rack::File, and use mechanisms
-
# like sendfile on the +path+.
-
-
1
class File
-
1
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
-
1
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
-
-
1
attr_accessor :root
-
1
attr_accessor :path
-
1
attr_accessor :cache_control
-
-
1
alias :to_path :path
-
-
1
def initialize(root, headers={}, default_mime = 'text/plain')
-
@root = root
-
@headers = headers
-
@default_mime = default_mime
-
end
-
-
1
def call(env)
-
dup._call(env)
-
end
-
-
1
F = ::File
-
-
1
def _call(env)
-
unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
-
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
-
end
-
-
path_info = Utils.unescape(env[PATH_INFO])
-
clean_path_info = Utils.clean_path_info(path_info)
-
-
@path = F.join(@root, clean_path_info)
-
-
available = begin
-
F.file?(@path) && F.readable?(@path)
-
rescue SystemCallError
-
false
-
end
-
-
if available
-
serving(env)
-
else
-
fail(404, "File not found: #{path_info}")
-
end
-
end
-
-
1
def serving(env)
-
if env["REQUEST_METHOD"] == "OPTIONS"
-
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
-
end
-
last_modified = F.mtime(@path).httpdate
-
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
-
-
headers = { "Last-Modified" => last_modified }
-
headers[CONTENT_TYPE] = mime_type if mime_type
-
-
# Set custom headers
-
@headers.each { |field, content| headers[field] = content } if @headers
-
-
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
-
-
size = filesize
-
-
ranges = Rack::Utils.byte_ranges(env, size)
-
if ranges.nil? || ranges.length > 1
-
# No ranges, or multiple ranges (which we don't support):
-
# TODO: Support multiple byte-ranges
-
response[0] = 200
-
@range = 0..size-1
-
elsif ranges.empty?
-
# Unsatisfiable. Return error, and file size:
-
response = fail(416, "Byte range unsatisfiable")
-
response[1]["Content-Range"] = "bytes */#{size}"
-
return response
-
else
-
# Partial content:
-
@range = ranges[0]
-
response[0] = 206
-
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
-
size = @range.end - @range.begin + 1
-
end
-
-
response[2] = [response_body] unless response_body.nil?
-
-
response[1][CONTENT_LENGTH] = size.to_s
-
response
-
end
-
-
1
def each
-
F.open(@path, "rb") do |file|
-
file.seek(@range.begin)
-
remaining_len = @range.end-@range.begin+1
-
while remaining_len > 0
-
part = file.read([8192, remaining_len].min)
-
break unless part
-
remaining_len -= part.length
-
-
yield part
-
end
-
end
-
end
-
-
1
private
-
-
1
def fail(status, body, headers = {})
-
body += "\n"
-
[
-
status,
-
{
-
CONTENT_TYPE => "text/plain",
-
CONTENT_LENGTH => body.size.to_s,
-
"X-Cascade" => "pass"
-
}.merge!(headers),
-
[body]
-
]
-
end
-
-
# The MIME type for the contents of the file located at @path
-
1
def mime_type
-
Mime.mime_type(F.extname(@path), @default_mime)
-
end
-
-
1
def filesize
-
# If response_body is present, use its size.
-
return Rack::Utils.bytesize(response_body) if response_body
-
-
# We check via File::size? whether this file provides size info
-
# via stat (e.g. /proc files often don't), otherwise we have to
-
# figure it out by reading the whole file into memory.
-
F.size?(@path) || Utils.bytesize(F.read(@path))
-
end
-
-
# By default, the response body for file requests is nil.
-
# In this case, the response body will be generated later
-
# from the file at @path
-
1
def response_body
-
nil
-
end
-
end
-
end
-
1
module Rack
-
# *Handlers* connect web servers with Rack.
-
#
-
# Rack includes Handlers for Thin, WEBrick, FastCGI, CGI, SCGI
-
# and LiteSpeed.
-
#
-
# Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>.
-
# A second optional hash can be passed to include server-specific
-
# configuration.
-
1
module Handler
-
1
def self.get(server)
-
return unless server
-
server = server.to_s
-
-
unless @handlers.include? server
-
load_error = try_require('rack/handler', server)
-
end
-
-
if klass = @handlers[server]
-
klass.split("::").inject(Object) { |o, x| o.const_get(x) }
-
else
-
_const_get(server, false)
-
end
-
-
rescue NameError => name_error
-
raise load_error || name_error
-
end
-
-
1
begin
-
1
::Object.const_get("Object", false)
-
1
def self._const_get(str, inherit = true)
-
const_get(str, inherit)
-
end
-
rescue
-
def self._const_get(str, inherit = true)
-
const_get(str)
-
end
-
end
-
-
-
# Select first available Rack handler given an `Array` of server names.
-
# Raises `LoadError` if no handler was found.
-
#
-
# > pick ['thin', 'webrick']
-
# => Rack::Handler::WEBrick
-
1
def self.pick(server_names)
-
server_names = Array(server_names)
-
server_names.each do |server_name|
-
begin
-
return get(server_name.to_s)
-
rescue LoadError, NameError
-
end
-
end
-
-
raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
-
end
-
-
1
def self.default(options = {})
-
# Guess.
-
if ENV.include?("PHP_FCGI_CHILDREN")
-
# We already speak FastCGI
-
options.delete :File
-
options.delete :Port
-
-
Rack::Handler::FastCGI
-
elsif ENV.include?(REQUEST_METHOD)
-
Rack::Handler::CGI
-
elsif ENV.include?("RACK_HANDLER")
-
self.get(ENV["RACK_HANDLER"])
-
else
-
pick ['thin', 'puma', 'webrick']
-
end
-
end
-
-
# Transforms server-name constants to their canonical form as filenames,
-
# then tries to require them but silences the LoadError if not found
-
#
-
# Naming convention:
-
#
-
# Foo # => 'foo'
-
# FooBar # => 'foo_bar.rb'
-
# FooBAR # => 'foobar.rb'
-
# FOObar # => 'foobar.rb'
-
# FOOBAR # => 'foobar.rb'
-
# FooBarBaz # => 'foo_bar_baz.rb'
-
1
def self.try_require(prefix, const_name)
-
file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
-
gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
-
-
require(::File.join(prefix, file))
-
nil
-
rescue LoadError => error
-
error
-
end
-
-
1
def self.register(server, klass)
-
9
@handlers ||= {}
-
9
@handlers[server.to_s] = klass.to_s
-
end
-
-
1
autoload :CGI, "rack/handler/cgi"
-
1
autoload :FastCGI, "rack/handler/fastcgi"
-
1
autoload :Mongrel, "rack/handler/mongrel"
-
1
autoload :EventedMongrel, "rack/handler/evented_mongrel"
-
1
autoload :SwiftipliedMongrel, "rack/handler/swiftiplied_mongrel"
-
1
autoload :WEBrick, "rack/handler/webrick"
-
1
autoload :LSWS, "rack/handler/lsws"
-
1
autoload :SCGI, "rack/handler/scgi"
-
1
autoload :Thin, "rack/handler/thin"
-
-
1
register 'cgi', 'Rack::Handler::CGI'
-
1
register 'fastcgi', 'Rack::Handler::FastCGI'
-
1
register 'mongrel', 'Rack::Handler::Mongrel'
-
1
register 'emongrel', 'Rack::Handler::EventedMongrel'
-
1
register 'smongrel', 'Rack::Handler::SwiftipliedMongrel'
-
1
register 'webrick', 'Rack::Handler::WEBrick'
-
1
register 'lsws', 'Rack::Handler::LSWS'
-
1
register 'scgi', 'Rack::Handler::SCGI'
-
1
register 'thin', 'Rack::Handler::Thin'
-
end
-
end
-
1
require 'webrick'
-
1
require 'stringio'
-
1
require 'rack/content_length'
-
-
# This monkey patch allows for applications to perform their own chunking
-
# through WEBrick::HTTPResponse iff rack is set to true.
-
1
class WEBrick::HTTPResponse
-
1
attr_accessor :rack
-
-
1
alias _rack_setup_header setup_header
-
1
def setup_header
-
72
app_chunking = rack && @header['transfer-encoding'] == 'chunked'
-
-
72
@chunked = app_chunking if app_chunking
-
-
72
_rack_setup_header
-
-
72
@chunked = false if app_chunking
-
end
-
end
-
-
1
module Rack
-
1
module Handler
-
1
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
-
1
def self.run(app, options={})
-
1
environment = ENV['RACK_ENV'] || 'development'
-
1
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-
1
options[:BindAddress] = options.delete(:Host) || default_host
-
1
options[:Port] ||= 8080
-
1
@server = ::WEBrick::HTTPServer.new(options)
-
1
@server.mount "/", Rack::Handler::WEBrick, app
-
1
yield @server if block_given?
-
1
@server.start
-
end
-
-
1
def self.valid_options
-
environment = ENV['RACK_ENV'] || 'development'
-
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-
{
-
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
-
"Port=PORT" => "Port to listen on (default: 8080)",
-
}
-
end
-
-
1
def self.shutdown
-
@server.shutdown
-
@server = nil
-
end
-
-
1
def initialize(server, app)
-
72
super server
-
72
@app = app
-
end
-
-
1
def service(req, res)
-
72
res.rack = true
-
72
env = req.meta_vars
-
1648
env.delete_if { |k, v| v.nil? }
-
-
72
rack_input = StringIO.new(req.body.to_s)
-
72
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
-
-
72
env.update({"rack.version" => Rack::VERSION,
-
"rack.input" => rack_input,
-
"rack.errors" => $stderr,
-
-
"rack.multithread" => true,
-
"rack.multiprocess" => false,
-
"rack.run_once" => false,
-
-
72
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http",
-
-
"rack.hijack?" => true,
-
"rack.hijack" => lambda { raise NotImplementedError, "only partial hijack is supported."},
-
"rack.hijack_io" => nil,
-
})
-
-
72
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
-
72
env[QUERY_STRING] ||= ""
-
72
unless env[PATH_INFO] == ""
-
72
path, n = req.request_uri.path, env["SCRIPT_NAME"].length
-
72
env[PATH_INFO] = path[n, path.length-n]
-
end
-
72
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[PATH_INFO]].join
-
-
72
status, headers, body = @app.call(env)
-
72
begin
-
72
res.status = status.to_i
-
72
headers.each { |k, vs|
-
418
next if k.downcase == "rack.hijack"
-
-
418
if k.downcase == "set-cookie"
-
21
res.cookies.concat vs.split("\n")
-
else
-
# Since WEBrick won't accept repeated headers,
-
# merge the values per RFC 1945 section 4.2.
-
397
res[k] = vs.split("\n").join(", ")
-
end
-
}
-
-
72
io_lambda = headers["rack.hijack"]
-
72
if io_lambda
-
rd, wr = IO.pipe
-
res.body = rd
-
res.chunked = true
-
io_lambda.call wr
-
72
elsif body.respond_to?(:to_path)
-
res.body = ::File.open(body.to_path, 'rb')
-
else
-
72
body.each { |part|
-
58
res.body << part
-
}
-
end
-
ensure
-
72
body.close if body.respond_to? :close
-
end
-
end
-
end
-
end
-
end
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
-
1
class Head
-
# Rack::Head returns an empty body for all HEAD requests. It leaves
-
# all other requests unchanged.
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
71
status, headers, body = @app.call(env)
-
-
71
if env[REQUEST_METHOD] == HEAD
-
[
-
status, headers, Rack::BodyProxy.new([]) do
-
body.close if body.respond_to? :close
-
end
-
]
-
else
-
71
[status, headers, body]
-
end
-
end
-
end
-
-
end
-
1
module Rack
-
1
class MethodOverride
-
1
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK)
-
-
1
METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
-
1
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
-
1
ALLOWED_METHODS = ["POST"]
-
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
71
if allowed_methods.include?(env[REQUEST_METHOD])
-
15
method = method_override(env)
-
15
if HTTP_METHODS.include?(method)
-
2
env["rack.methodoverride.original_method"] = env[REQUEST_METHOD]
-
2
env[REQUEST_METHOD] = method
-
end
-
end
-
-
71
@app.call(env)
-
end
-
-
1
def method_override(env)
-
15
req = Request.new(env)
-
15
method = method_override_param(req) ||
-
env[HTTP_METHOD_OVERRIDE_HEADER]
-
15
method.to_s.upcase
-
end
-
-
1
private
-
-
1
def allowed_methods
-
71
ALLOWED_METHODS
-
end
-
-
1
def method_override_param(req)
-
15
req.POST[METHOD_OVERRIDE_PARAM_KEY]
-
rescue Utils::InvalidParameterError, Utils::ParameterTypeError
-
end
-
end
-
end
-
1
module Rack
-
1
module Mime
-
# Returns String with mime type if found, otherwise use +fallback+.
-
# +ext+ should be filename extension in the '.ext' format that
-
# File.extname(file) returns.
-
# +fallback+ may be any object
-
#
-
# Also see the documentation for MIME_TYPES
-
#
-
# Usage:
-
# Rack::Mime.mime_type('.foo')
-
#
-
# This is a shortcut for:
-
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
-
-
1
def mime_type(ext, fallback='application/octet-stream')
-
71
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
-
end
-
1
module_function :mime_type
-
-
# Returns true if the given value is a mime match for the given mime match
-
# specification, false otherwise.
-
#
-
# Rack::Mime.match?('text/html', 'text/*') => true
-
# Rack::Mime.match?('text/plain', '*') => true
-
# Rack::Mime.match?('text/html', 'application/json') => false
-
-
1
def match?(value, matcher)
-
v1, v2 = value.split('/', 2)
-
m1, m2 = matcher.split('/', 2)
-
-
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
-
end
-
1
module_function :match?
-
-
# List of most common mime-types, selected various sources
-
# according to their usefulness in a webserving scope for Ruby
-
# users.
-
#
-
# To amend this list with your local mime.types list you can use:
-
#
-
# require 'webrick/httputils'
-
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
-
# Rack::Mime::MIME_TYPES.merge!(list)
-
#
-
# N.B. On Ubuntu the mime.types file does not include the leading period, so
-
# users may need to modify the data before merging into the hash.
-
#
-
# To add the list mongrel provides, use:
-
#
-
# require 'mongrel/handlers'
-
# Rack::Mime::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
-
-
1
MIME_TYPES = {
-
".123" => "application/vnd.lotus-1-2-3",
-
".3dml" => "text/vnd.in3d.3dml",
-
".3g2" => "video/3gpp2",
-
".3gp" => "video/3gpp",
-
".a" => "application/octet-stream",
-
".acc" => "application/vnd.americandynamics.acc",
-
".ace" => "application/x-ace-compressed",
-
".acu" => "application/vnd.acucobol",
-
".aep" => "application/vnd.audiograph",
-
".afp" => "application/vnd.ibm.modcap",
-
".ai" => "application/postscript",
-
".aif" => "audio/x-aiff",
-
".aiff" => "audio/x-aiff",
-
".ami" => "application/vnd.amiga.ami",
-
".appcache" => "text/cache-manifest",
-
".apr" => "application/vnd.lotus-approach",
-
".asc" => "application/pgp-signature",
-
".asf" => "video/x-ms-asf",
-
".asm" => "text/x-asm",
-
".aso" => "application/vnd.accpac.simply.aso",
-
".asx" => "video/x-ms-asf",
-
".atc" => "application/vnd.acucorp",
-
".atom" => "application/atom+xml",
-
".atomcat" => "application/atomcat+xml",
-
".atomsvc" => "application/atomsvc+xml",
-
".atx" => "application/vnd.antix.game-component",
-
".au" => "audio/basic",
-
".avi" => "video/x-msvideo",
-
".bat" => "application/x-msdownload",
-
".bcpio" => "application/x-bcpio",
-
".bdm" => "application/vnd.syncml.dm+wbxml",
-
".bh2" => "application/vnd.fujitsu.oasysprs",
-
".bin" => "application/octet-stream",
-
".bmi" => "application/vnd.bmi",
-
".bmp" => "image/bmp",
-
".box" => "application/vnd.previewsystems.box",
-
".btif" => "image/prs.btif",
-
".bz" => "application/x-bzip",
-
".bz2" => "application/x-bzip2",
-
".c" => "text/x-c",
-
".c4g" => "application/vnd.clonk.c4group",
-
".cab" => "application/vnd.ms-cab-compressed",
-
".cc" => "text/x-c",
-
".ccxml" => "application/ccxml+xml",
-
".cdbcmsg" => "application/vnd.contact.cmsg",
-
".cdkey" => "application/vnd.mediastation.cdkey",
-
".cdx" => "chemical/x-cdx",
-
".cdxml" => "application/vnd.chemdraw+xml",
-
".cdy" => "application/vnd.cinderella",
-
".cer" => "application/pkix-cert",
-
".cgm" => "image/cgm",
-
".chat" => "application/x-chat",
-
".chm" => "application/vnd.ms-htmlhelp",
-
".chrt" => "application/vnd.kde.kchart",
-
".cif" => "chemical/x-cif",
-
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
-
".cil" => "application/vnd.ms-artgalry",
-
".cla" => "application/vnd.claymore",
-
".class" => "application/octet-stream",
-
".clkk" => "application/vnd.crick.clicker.keyboard",
-
".clkp" => "application/vnd.crick.clicker.palette",
-
".clkt" => "application/vnd.crick.clicker.template",
-
".clkw" => "application/vnd.crick.clicker.wordbank",
-
".clkx" => "application/vnd.crick.clicker",
-
".clp" => "application/x-msclip",
-
".cmc" => "application/vnd.cosmocaller",
-
".cmdf" => "chemical/x-cmdf",
-
".cml" => "chemical/x-cml",
-
".cmp" => "application/vnd.yellowriver-custom-menu",
-
".cmx" => "image/x-cmx",
-
".com" => "application/x-msdownload",
-
".conf" => "text/plain",
-
".cpio" => "application/x-cpio",
-
".cpp" => "text/x-c",
-
".cpt" => "application/mac-compactpro",
-
".crd" => "application/x-mscardfile",
-
".crl" => "application/pkix-crl",
-
".crt" => "application/x-x509-ca-cert",
-
".csh" => "application/x-csh",
-
".csml" => "chemical/x-csml",
-
".csp" => "application/vnd.commonspace",
-
".css" => "text/css",
-
".csv" => "text/csv",
-
".curl" => "application/vnd.curl",
-
".cww" => "application/prs.cww",
-
".cxx" => "text/x-c",
-
".daf" => "application/vnd.mobius.daf",
-
".davmount" => "application/davmount+xml",
-
".dcr" => "application/x-director",
-
".dd2" => "application/vnd.oma.dd2+xml",
-
".ddd" => "application/vnd.fujixerox.ddd",
-
".deb" => "application/x-debian-package",
-
".der" => "application/x-x509-ca-cert",
-
".dfac" => "application/vnd.dreamfactory",
-
".diff" => "text/x-diff",
-
".dis" => "application/vnd.mobius.dis",
-
".djv" => "image/vnd.djvu",
-
".djvu" => "image/vnd.djvu",
-
".dll" => "application/x-msdownload",
-
".dmg" => "application/octet-stream",
-
".dna" => "application/vnd.dna",
-
".doc" => "application/msword",
-
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-
".dot" => "application/msword",
-
".dp" => "application/vnd.osgi.dp",
-
".dpg" => "application/vnd.dpgraph",
-
".dsc" => "text/prs.lines.tag",
-
".dtd" => "application/xml-dtd",
-
".dts" => "audio/vnd.dts",
-
".dtshd" => "audio/vnd.dts.hd",
-
".dv" => "video/x-dv",
-
".dvi" => "application/x-dvi",
-
".dwf" => "model/vnd.dwf",
-
".dwg" => "image/vnd.dwg",
-
".dxf" => "image/vnd.dxf",
-
".dxp" => "application/vnd.spotfire.dxp",
-
".ear" => "application/java-archive",
-
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
-
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
-
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
-
".ecma" => "application/ecmascript",
-
".edm" => "application/vnd.novadigm.edm",
-
".edx" => "application/vnd.novadigm.edx",
-
".efif" => "application/vnd.picsel",
-
".ei6" => "application/vnd.pg.osasli",
-
".eml" => "message/rfc822",
-
".eol" => "audio/vnd.digital-winds",
-
".eot" => "application/vnd.ms-fontobject",
-
".eps" => "application/postscript",
-
".es3" => "application/vnd.eszigno3+xml",
-
".esf" => "application/vnd.epson.esf",
-
".etx" => "text/x-setext",
-
".exe" => "application/x-msdownload",
-
".ext" => "application/vnd.novadigm.ext",
-
".ez" => "application/andrew-inset",
-
".ez2" => "application/vnd.ezpix-album",
-
".ez3" => "application/vnd.ezpix-package",
-
".f" => "text/x-fortran",
-
".f77" => "text/x-fortran",
-
".f90" => "text/x-fortran",
-
".fbs" => "image/vnd.fastbidsheet",
-
".fdf" => "application/vnd.fdf",
-
".fe_launch" => "application/vnd.denovo.fcselayout-link",
-
".fg5" => "application/vnd.fujitsu.oasysgp",
-
".fli" => "video/x-fli",
-
".flo" => "application/vnd.micrografx.flo",
-
".flv" => "video/x-flv",
-
".flw" => "application/vnd.kde.kivio",
-
".flx" => "text/vnd.fmi.flexstor",
-
".fly" => "text/vnd.fly",
-
".fm" => "application/vnd.framemaker",
-
".fnc" => "application/vnd.frogans.fnc",
-
".for" => "text/x-fortran",
-
".fpx" => "image/vnd.fpx",
-
".fsc" => "application/vnd.fsc.weblaunch",
-
".fst" => "image/vnd.fst",
-
".ftc" => "application/vnd.fluxtime.clip",
-
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
-
".fvt" => "video/vnd.fvt",
-
".fzs" => "application/vnd.fuzzysheet",
-
".g3" => "image/g3fax",
-
".gac" => "application/vnd.groove-account",
-
".gdl" => "model/vnd.gdl",
-
".gem" => "application/octet-stream",
-
".gemspec" => "text/x-script.ruby",
-
".ghf" => "application/vnd.groove-help",
-
".gif" => "image/gif",
-
".gim" => "application/vnd.groove-identity-message",
-
".gmx" => "application/vnd.gmx",
-
".gph" => "application/vnd.flographit",
-
".gqf" => "application/vnd.grafeq",
-
".gram" => "application/srgs",
-
".grv" => "application/vnd.groove-injector",
-
".grxml" => "application/srgs+xml",
-
".gtar" => "application/x-gtar",
-
".gtm" => "application/vnd.groove-tool-message",
-
".gtw" => "model/vnd.gtw",
-
".gv" => "text/vnd.graphviz",
-
".gz" => "application/x-gzip",
-
".h" => "text/x-c",
-
".h261" => "video/h261",
-
".h263" => "video/h263",
-
".h264" => "video/h264",
-
".hbci" => "application/vnd.hbci",
-
".hdf" => "application/x-hdf",
-
".hh" => "text/x-c",
-
".hlp" => "application/winhlp",
-
".hpgl" => "application/vnd.hp-hpgl",
-
".hpid" => "application/vnd.hp-hpid",
-
".hps" => "application/vnd.hp-hps",
-
".hqx" => "application/mac-binhex40",
-
".htc" => "text/x-component",
-
".htke" => "application/vnd.kenameaapp",
-
".htm" => "text/html",
-
".html" => "text/html",
-
".hvd" => "application/vnd.yamaha.hv-dic",
-
".hvp" => "application/vnd.yamaha.hv-voice",
-
".hvs" => "application/vnd.yamaha.hv-script",
-
".icc" => "application/vnd.iccprofile",
-
".ice" => "x-conference/x-cooltalk",
-
".ico" => "image/vnd.microsoft.icon",
-
".ics" => "text/calendar",
-
".ief" => "image/ief",
-
".ifb" => "text/calendar",
-
".ifm" => "application/vnd.shana.informed.formdata",
-
".igl" => "application/vnd.igloader",
-
".igs" => "model/iges",
-
".igx" => "application/vnd.micrografx.igx",
-
".iif" => "application/vnd.shana.informed.interchange",
-
".imp" => "application/vnd.accpac.simply.imp",
-
".ims" => "application/vnd.ms-ims",
-
".ipk" => "application/vnd.shana.informed.package",
-
".irm" => "application/vnd.ibm.rights-management",
-
".irp" => "application/vnd.irepository.package+xml",
-
".iso" => "application/octet-stream",
-
".itp" => "application/vnd.shana.informed.formtemplate",
-
".ivp" => "application/vnd.immervision-ivp",
-
".ivu" => "application/vnd.immervision-ivu",
-
".jad" => "text/vnd.sun.j2me.app-descriptor",
-
".jam" => "application/vnd.jam",
-
".jar" => "application/java-archive",
-
".java" => "text/x-java-source",
-
".jisp" => "application/vnd.jisp",
-
".jlt" => "application/vnd.hp-jlyt",
-
".jnlp" => "application/x-java-jnlp-file",
-
".joda" => "application/vnd.joost.joda-archive",
-
".jp2" => "image/jp2",
-
".jpeg" => "image/jpeg",
-
".jpg" => "image/jpeg",
-
".jpgv" => "video/jpeg",
-
".jpm" => "video/jpm",
-
".js" => "application/javascript",
-
".json" => "application/json",
-
".karbon" => "application/vnd.kde.karbon",
-
".kfo" => "application/vnd.kde.kformula",
-
".kia" => "application/vnd.kidspiration",
-
".kml" => "application/vnd.google-earth.kml+xml",
-
".kmz" => "application/vnd.google-earth.kmz",
-
".kne" => "application/vnd.kinar",
-
".kon" => "application/vnd.kde.kontour",
-
".kpr" => "application/vnd.kde.kpresenter",
-
".ksp" => "application/vnd.kde.kspread",
-
".ktz" => "application/vnd.kahootz",
-
".kwd" => "application/vnd.kde.kword",
-
".latex" => "application/x-latex",
-
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
-
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
-
".les" => "application/vnd.hhe.lesson-player",
-
".link66" => "application/vnd.route66.link66+xml",
-
".log" => "text/plain",
-
".lostxml" => "application/lost+xml",
-
".lrm" => "application/vnd.ms-lrm",
-
".ltf" => "application/vnd.frogans.ltf",
-
".lvp" => "audio/vnd.lucent.voice",
-
".lwp" => "application/vnd.lotus-wordpro",
-
".m3u" => "audio/x-mpegurl",
-
".m4a" => "audio/mp4a-latm",
-
".m4v" => "video/mp4",
-
".ma" => "application/mathematica",
-
".mag" => "application/vnd.ecowin.chart",
-
".man" => "text/troff",
-
".manifest" => "text/cache-manifest",
-
".mathml" => "application/mathml+xml",
-
".mbk" => "application/vnd.mobius.mbk",
-
".mbox" => "application/mbox",
-
".mc1" => "application/vnd.medcalcdata",
-
".mcd" => "application/vnd.mcd",
-
".mdb" => "application/x-msaccess",
-
".mdi" => "image/vnd.ms-modi",
-
".mdoc" => "text/troff",
-
".me" => "text/troff",
-
".mfm" => "application/vnd.mfmp",
-
".mgz" => "application/vnd.proteus.magazine",
-
".mid" => "audio/midi",
-
".midi" => "audio/midi",
-
".mif" => "application/vnd.mif",
-
".mime" => "message/rfc822",
-
".mj2" => "video/mj2",
-
".mlp" => "application/vnd.dolby.mlp",
-
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
-
".mmf" => "application/vnd.smaf",
-
".mml" => "application/mathml+xml",
-
".mmr" => "image/vnd.fujixerox.edmics-mmr",
-
".mng" => "video/x-mng",
-
".mny" => "application/x-msmoney",
-
".mov" => "video/quicktime",
-
".movie" => "video/x-sgi-movie",
-
".mp3" => "audio/mpeg",
-
".mp4" => "video/mp4",
-
".mp4a" => "audio/mp4",
-
".mp4s" => "application/mp4",
-
".mp4v" => "video/mp4",
-
".mpc" => "application/vnd.mophun.certificate",
-
".mpeg" => "video/mpeg",
-
".mpg" => "video/mpeg",
-
".mpga" => "audio/mpeg",
-
".mpkg" => "application/vnd.apple.installer+xml",
-
".mpm" => "application/vnd.blueice.multipass",
-
".mpn" => "application/vnd.mophun.application",
-
".mpp" => "application/vnd.ms-project",
-
".mpy" => "application/vnd.ibm.minipay",
-
".mqy" => "application/vnd.mobius.mqy",
-
".mrc" => "application/marc",
-
".ms" => "text/troff",
-
".mscml" => "application/mediaservercontrol+xml",
-
".mseq" => "application/vnd.mseq",
-
".msf" => "application/vnd.epson.msf",
-
".msh" => "model/mesh",
-
".msi" => "application/x-msdownload",
-
".msl" => "application/vnd.mobius.msl",
-
".msty" => "application/vnd.muvee.style",
-
".mts" => "model/vnd.mts",
-
".mus" => "application/vnd.musician",
-
".mvb" => "application/x-msmediaview",
-
".mwf" => "application/vnd.mfer",
-
".mxf" => "application/mxf",
-
".mxl" => "application/vnd.recordare.musicxml",
-
".mxml" => "application/xv+xml",
-
".mxs" => "application/vnd.triscape.mxs",
-
".mxu" => "video/vnd.mpegurl",
-
".n" => "application/vnd.nokia.n-gage.symbian.install",
-
".nc" => "application/x-netcdf",
-
".ngdat" => "application/vnd.nokia.n-gage.data",
-
".nlu" => "application/vnd.neurolanguage.nlu",
-
".nml" => "application/vnd.enliven",
-
".nnd" => "application/vnd.noblenet-directory",
-
".nns" => "application/vnd.noblenet-sealer",
-
".nnw" => "application/vnd.noblenet-web",
-
".npx" => "image/vnd.net-fpx",
-
".nsf" => "application/vnd.lotus-notes",
-
".oa2" => "application/vnd.fujitsu.oasys2",
-
".oa3" => "application/vnd.fujitsu.oasys3",
-
".oas" => "application/vnd.fujitsu.oasys",
-
".obd" => "application/x-msbinder",
-
".oda" => "application/oda",
-
".odc" => "application/vnd.oasis.opendocument.chart",
-
".odf" => "application/vnd.oasis.opendocument.formula",
-
".odg" => "application/vnd.oasis.opendocument.graphics",
-
".odi" => "application/vnd.oasis.opendocument.image",
-
".odp" => "application/vnd.oasis.opendocument.presentation",
-
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
-
".odt" => "application/vnd.oasis.opendocument.text",
-
".oga" => "audio/ogg",
-
".ogg" => "application/ogg",
-
".ogv" => "video/ogg",
-
".ogx" => "application/ogg",
-
".org" => "application/vnd.lotus-organizer",
-
".otc" => "application/vnd.oasis.opendocument.chart-template",
-
".otf" => "application/vnd.oasis.opendocument.formula-template",
-
".otg" => "application/vnd.oasis.opendocument.graphics-template",
-
".oth" => "application/vnd.oasis.opendocument.text-web",
-
".oti" => "application/vnd.oasis.opendocument.image-template",
-
".otm" => "application/vnd.oasis.opendocument.text-master",
-
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
-
".ott" => "application/vnd.oasis.opendocument.text-template",
-
".oxt" => "application/vnd.openofficeorg.extension",
-
".p" => "text/x-pascal",
-
".p10" => "application/pkcs10",
-
".p12" => "application/x-pkcs12",
-
".p7b" => "application/x-pkcs7-certificates",
-
".p7m" => "application/pkcs7-mime",
-
".p7r" => "application/x-pkcs7-certreqresp",
-
".p7s" => "application/pkcs7-signature",
-
".pas" => "text/x-pascal",
-
".pbd" => "application/vnd.powerbuilder6",
-
".pbm" => "image/x-portable-bitmap",
-
".pcl" => "application/vnd.hp-pcl",
-
".pclxl" => "application/vnd.hp-pclxl",
-
".pcx" => "image/x-pcx",
-
".pdb" => "chemical/x-pdb",
-
".pdf" => "application/pdf",
-
".pem" => "application/x-x509-ca-cert",
-
".pfr" => "application/font-tdpfr",
-
".pgm" => "image/x-portable-graymap",
-
".pgn" => "application/x-chess-pgn",
-
".pgp" => "application/pgp-encrypted",
-
".pic" => "image/x-pict",
-
".pict" => "image/pict",
-
".pkg" => "application/octet-stream",
-
".pki" => "application/pkixcmp",
-
".pkipath" => "application/pkix-pkipath",
-
".pl" => "text/x-script.perl",
-
".plb" => "application/vnd.3gpp.pic-bw-large",
-
".plc" => "application/vnd.mobius.plc",
-
".plf" => "application/vnd.pocketlearn",
-
".pls" => "application/pls+xml",
-
".pm" => "text/x-script.perl-module",
-
".pml" => "application/vnd.ctc-posml",
-
".png" => "image/png",
-
".pnm" => "image/x-portable-anymap",
-
".pntg" => "image/x-macpaint",
-
".portpkg" => "application/vnd.macports.portpkg",
-
".ppd" => "application/vnd.cups-ppd",
-
".ppm" => "image/x-portable-pixmap",
-
".pps" => "application/vnd.ms-powerpoint",
-
".ppt" => "application/vnd.ms-powerpoint",
-
".prc" => "application/vnd.palm",
-
".pre" => "application/vnd.lotus-freelance",
-
".prf" => "application/pics-rules",
-
".ps" => "application/postscript",
-
".psb" => "application/vnd.3gpp.pic-bw-small",
-
".psd" => "image/vnd.adobe.photoshop",
-
".ptid" => "application/vnd.pvi.ptid1",
-
".pub" => "application/x-mspublisher",
-
".pvb" => "application/vnd.3gpp.pic-bw-var",
-
".pwn" => "application/vnd.3m.post-it-notes",
-
".py" => "text/x-script.python",
-
".pya" => "audio/vnd.ms-playready.media.pya",
-
".pyv" => "video/vnd.ms-playready.media.pyv",
-
".qam" => "application/vnd.epson.quickanime",
-
".qbo" => "application/vnd.intu.qbo",
-
".qfx" => "application/vnd.intu.qfx",
-
".qps" => "application/vnd.publishare-delta-tree",
-
".qt" => "video/quicktime",
-
".qtif" => "image/x-quicktime",
-
".qxd" => "application/vnd.quark.quarkxpress",
-
".ra" => "audio/x-pn-realaudio",
-
".rake" => "text/x-script.ruby",
-
".ram" => "audio/x-pn-realaudio",
-
".rar" => "application/x-rar-compressed",
-
".ras" => "image/x-cmu-raster",
-
".rb" => "text/x-script.ruby",
-
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
-
".rdf" => "application/rdf+xml",
-
".rdz" => "application/vnd.data-vision.rdz",
-
".rep" => "application/vnd.businessobjects",
-
".rgb" => "image/x-rgb",
-
".rif" => "application/reginfo+xml",
-
".rl" => "application/resource-lists+xml",
-
".rlc" => "image/vnd.fujixerox.edmics-rlc",
-
".rld" => "application/resource-lists-diff+xml",
-
".rm" => "application/vnd.rn-realmedia",
-
".rmp" => "audio/x-pn-realaudio-plugin",
-
".rms" => "application/vnd.jcp.javame.midlet-rms",
-
".rnc" => "application/relax-ng-compact-syntax",
-
".roff" => "text/troff",
-
".rpm" => "application/x-redhat-package-manager",
-
".rpss" => "application/vnd.nokia.radio-presets",
-
".rpst" => "application/vnd.nokia.radio-preset",
-
".rq" => "application/sparql-query",
-
".rs" => "application/rls-services+xml",
-
".rsd" => "application/rsd+xml",
-
".rss" => "application/rss+xml",
-
".rtf" => "application/rtf",
-
".rtx" => "text/richtext",
-
".ru" => "text/x-script.ruby",
-
".s" => "text/x-asm",
-
".saf" => "application/vnd.yamaha.smaf-audio",
-
".sbml" => "application/sbml+xml",
-
".sc" => "application/vnd.ibm.secure-container",
-
".scd" => "application/x-msschedule",
-
".scm" => "application/vnd.lotus-screencam",
-
".scq" => "application/scvp-cv-request",
-
".scs" => "application/scvp-cv-response",
-
".sdkm" => "application/vnd.solent.sdkm+xml",
-
".sdp" => "application/sdp",
-
".see" => "application/vnd.seemail",
-
".sema" => "application/vnd.sema",
-
".semd" => "application/vnd.semd",
-
".semf" => "application/vnd.semf",
-
".setpay" => "application/set-payment-initiation",
-
".setreg" => "application/set-registration-initiation",
-
".sfd" => "application/vnd.hydrostatix.sof-data",
-
".sfs" => "application/vnd.spotfire.sfs",
-
".sgm" => "text/sgml",
-
".sgml" => "text/sgml",
-
".sh" => "application/x-sh",
-
".shar" => "application/x-shar",
-
".shf" => "application/shf+xml",
-
".sig" => "application/pgp-signature",
-
".sit" => "application/x-stuffit",
-
".sitx" => "application/x-stuffitx",
-
".skp" => "application/vnd.koan",
-
".slt" => "application/vnd.epson.salt",
-
".smi" => "application/smil+xml",
-
".snd" => "audio/basic",
-
".so" => "application/octet-stream",
-
".spf" => "application/vnd.yamaha.smaf-phrase",
-
".spl" => "application/x-futuresplash",
-
".spot" => "text/vnd.in3d.spot",
-
".spp" => "application/scvp-vp-response",
-
".spq" => "application/scvp-vp-request",
-
".src" => "application/x-wais-source",
-
".srx" => "application/sparql-results+xml",
-
".sse" => "application/vnd.kodak-descriptor",
-
".ssf" => "application/vnd.epson.ssf",
-
".ssml" => "application/ssml+xml",
-
".stf" => "application/vnd.wt.stf",
-
".stk" => "application/hyperstudio",
-
".str" => "application/vnd.pg.format",
-
".sus" => "application/vnd.sus-calendar",
-
".sv4cpio" => "application/x-sv4cpio",
-
".sv4crc" => "application/x-sv4crc",
-
".svd" => "application/vnd.svd",
-
".svg" => "image/svg+xml",
-
".svgz" => "image/svg+xml",
-
".swf" => "application/x-shockwave-flash",
-
".swi" => "application/vnd.arastra.swi",
-
".t" => "text/troff",
-
".tao" => "application/vnd.tao.intent-module-archive",
-
".tar" => "application/x-tar",
-
".tbz" => "application/x-bzip-compressed-tar",
-
".tcap" => "application/vnd.3gpp2.tcap",
-
".tcl" => "application/x-tcl",
-
".tex" => "application/x-tex",
-
".texi" => "application/x-texinfo",
-
".texinfo" => "application/x-texinfo",
-
".text" => "text/plain",
-
".tif" => "image/tiff",
-
".tiff" => "image/tiff",
-
".tmo" => "application/vnd.tmobile-livetv",
-
".torrent" => "application/x-bittorrent",
-
".tpl" => "application/vnd.groove-tool-template",
-
".tpt" => "application/vnd.trid.tpt",
-
".tr" => "text/troff",
-
".tra" => "application/vnd.trueapp",
-
".trm" => "application/x-msterminal",
-
".tsv" => "text/tab-separated-values",
-
".ttf" => "application/octet-stream",
-
".twd" => "application/vnd.simtech-mindmapper",
-
".txd" => "application/vnd.genomatix.tuxedo",
-
".txf" => "application/vnd.mobius.txf",
-
".txt" => "text/plain",
-
".ufd" => "application/vnd.ufdl",
-
".umj" => "application/vnd.umajin",
-
".unityweb" => "application/vnd.unity",
-
".uoml" => "application/vnd.uoml+xml",
-
".uri" => "text/uri-list",
-
".ustar" => "application/x-ustar",
-
".utz" => "application/vnd.uiq.theme",
-
".uu" => "text/x-uuencode",
-
".vcd" => "application/x-cdlink",
-
".vcf" => "text/x-vcard",
-
".vcg" => "application/vnd.groove-vcard",
-
".vcs" => "text/x-vcalendar",
-
".vcx" => "application/vnd.vcx",
-
".vis" => "application/vnd.visionary",
-
".viv" => "video/vnd.vivo",
-
".vrml" => "model/vrml",
-
".vsd" => "application/vnd.visio",
-
".vsf" => "application/vnd.vsf",
-
".vtu" => "model/vnd.vtu",
-
".vxml" => "application/voicexml+xml",
-
".war" => "application/java-archive",
-
".wav" => "audio/x-wav",
-
".wax" => "audio/x-ms-wax",
-
".wbmp" => "image/vnd.wap.wbmp",
-
".wbs" => "application/vnd.criticaltools.wbs+xml",
-
".wbxml" => "application/vnd.wap.wbxml",
-
".webm" => "video/webm",
-
".wm" => "video/x-ms-wm",
-
".wma" => "audio/x-ms-wma",
-
".wmd" => "application/x-ms-wmd",
-
".wmf" => "application/x-msmetafile",
-
".wml" => "text/vnd.wap.wml",
-
".wmlc" => "application/vnd.wap.wmlc",
-
".wmls" => "text/vnd.wap.wmlscript",
-
".wmlsc" => "application/vnd.wap.wmlscriptc",
-
".wmv" => "video/x-ms-wmv",
-
".wmx" => "video/x-ms-wmx",
-
".wmz" => "application/x-ms-wmz",
-
".woff" => "application/font-woff",
-
".woff2" => "application/font-woff2",
-
".wpd" => "application/vnd.wordperfect",
-
".wpl" => "application/vnd.ms-wpl",
-
".wps" => "application/vnd.ms-works",
-
".wqd" => "application/vnd.wqd",
-
".wri" => "application/x-mswrite",
-
".wrl" => "model/vrml",
-
".wsdl" => "application/wsdl+xml",
-
".wspolicy" => "application/wspolicy+xml",
-
".wtb" => "application/vnd.webturbo",
-
".wvx" => "video/x-ms-wvx",
-
".x3d" => "application/vnd.hzn-3d-crossword",
-
".xar" => "application/vnd.xara",
-
".xbd" => "application/vnd.fujixerox.docuworks.binder",
-
".xbm" => "image/x-xbitmap",
-
".xdm" => "application/vnd.syncml.dm+xml",
-
".xdp" => "application/vnd.adobe.xdp+xml",
-
".xdw" => "application/vnd.fujixerox.docuworks",
-
".xenc" => "application/xenc+xml",
-
".xer" => "application/patch-ops-error+xml",
-
".xfdf" => "application/vnd.adobe.xfdf",
-
".xfdl" => "application/vnd.xfdl",
-
".xhtml" => "application/xhtml+xml",
-
".xif" => "image/vnd.xiff",
-
".xls" => "application/vnd.ms-excel",
-
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-
".xml" => "application/xml",
-
".xo" => "application/vnd.olpc-sugar",
-
".xop" => "application/xop+xml",
-
".xpm" => "image/x-xpixmap",
-
".xpr" => "application/vnd.is-xpr",
-
".xps" => "application/vnd.ms-xpsdocument",
-
".xpw" => "application/vnd.intercon.formnet",
-
".xsl" => "application/xml",
-
".xslt" => "application/xslt+xml",
-
".xsm" => "application/vnd.syncml+xml",
-
".xspf" => "application/xspf+xml",
-
".xul" => "application/vnd.mozilla.xul+xml",
-
".xwd" => "image/x-xwindowdump",
-
".xyz" => "chemical/x-xyz",
-
".yaml" => "text/yaml",
-
".yml" => "text/yaml",
-
".zaz" => "application/vnd.zzazz.deck+xml",
-
".zip" => "application/zip",
-
".zmm" => "application/vnd.handheld-entertainment+xml",
-
}
-
end
-
end
-
1
module Rack
-
# A multipart form data parser, adapted from IOWA.
-
#
-
# Usually, Rack::Request#POST takes care of calling this.
-
1
module Multipart
-
1
autoload :UploadedFile, 'rack/multipart/uploaded_file'
-
1
autoload :Parser, 'rack/multipart/parser'
-
1
autoload :Generator, 'rack/multipart/generator'
-
-
1
EOL = "\r\n"
-
1
MULTIPART_BOUNDARY = "AaB03x"
-
1
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
-
1
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
-
1
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
-
1
DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
-
1
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
-
1
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
-
1
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
-
1
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
-
1
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
-
1
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
-
-
1
class << self
-
1
def parse_multipart(env)
-
15
Parser.create(env).parse
-
end
-
-
1
def build_multipart(params, first = true)
-
Generator.new(params, first).dump
-
end
-
end
-
-
end
-
end
-
1
require 'rack/utils'
-
-
1
module Rack
-
1
module Multipart
-
1
class MultipartPartLimitError < Errno::EMFILE; end
-
-
1
class Parser
-
1
BUFSIZE = 16384
-
-
1
DUMMY = Struct.new(:parse).new
-
-
1
def self.create(env)
-
15
return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
-
-
io = env['rack.input']
-
io.rewind
-
-
content_length = env['CONTENT_LENGTH']
-
content_length = content_length.to_i if content_length
-
-
tempfile = env['rack.multipart.tempfile_factory'] ||
-
lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename)]) }
-
bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
-
-
new($1, io, content_length, env, tempfile, bufsize)
-
end
-
-
1
def initialize(boundary, io, content_length, env, tempfile, bufsize)
-
@buf = ""
-
-
if @buf.respond_to? :force_encoding
-
@buf.force_encoding Encoding::ASCII_8BIT
-
end
-
-
@params = Utils::KeySpaceConstrainedParams.new
-
@boundary = "--#{boundary}"
-
@io = io
-
@content_length = content_length
-
@boundary_size = Utils.bytesize(@boundary) + EOL.size
-
@env = env
-
@tempfile = tempfile
-
@bufsize = bufsize
-
-
if @content_length
-
@content_length -= @boundary_size
-
end
-
-
@rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
-
@full_boundary = @boundary + EOL
-
end
-
-
1
def parse
-
fast_forward_to_first_boundary
-
-
opened_files = 0
-
loop do
-
-
head, filename, content_type, name, body =
-
get_current_head_and_filename_and_content_type_and_name_and_body
-
-
if Utils.multipart_part_limit > 0
-
opened_files += 1 if filename
-
raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
-
end
-
-
# Save the rest.
-
if i = @buf.index(rx)
-
body << @buf.slice!(0, i)
-
@buf.slice!(0, @boundary_size+2)
-
-
@content_length = -1 if $1 == "--"
-
end
-
-
get_data(filename, body, content_type, name, head) do |data|
-
tag_multipart_encoding(filename, content_type, name, data)
-
-
Utils.normalize_params(@params, name, data)
-
end
-
-
# break if we're at the end of a buffer, but not if it is the end of a field
-
break if (@buf.empty? && $1 != EOL) || @content_length == -1
-
end
-
-
@io.rewind
-
-
@params.to_params_hash
-
end
-
-
1
private
-
1
def full_boundary; @full_boundary; end
-
-
1
def rx; @rx; end
-
-
1
def fast_forward_to_first_boundary
-
loop do
-
content = @io.read(@bufsize)
-
raise EOFError, "bad content body" unless content
-
@buf << content
-
-
while @buf.gsub!(/\A([^\n]*\n)/, '')
-
read_buffer = $1
-
return if read_buffer == full_boundary
-
end
-
-
raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
-
end
-
end
-
-
1
def get_current_head_and_filename_and_content_type_and_name_and_body
-
head = nil
-
body = ''
-
-
if body.respond_to? :force_encoding
-
body.force_encoding Encoding::ASCII_8BIT
-
end
-
-
filename = content_type = name = nil
-
-
until head && @buf =~ rx
-
if !head && i = @buf.index(EOL+EOL)
-
head = @buf.slice!(0, i+2) # First \r\n
-
-
@buf.slice!(0, 2) # Second \r\n
-
-
content_type = head[MULTIPART_CONTENT_TYPE, 1]
-
name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
-
-
filename = get_filename(head)
-
-
if name.nil? || name.empty? && filename
-
name = filename
-
end
-
-
if filename
-
(@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
-
body.binmode if body.respond_to?(:binmode)
-
end
-
-
next
-
end
-
-
# Save the read body part.
-
if head && (@boundary_size+4 < @buf.size)
-
body << @buf.slice!(0, @buf.size - (@boundary_size+4))
-
end
-
-
content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
-
raise EOFError, "bad content body" if content.nil? || content.empty?
-
-
@buf << content
-
@content_length -= content.size if @content_length
-
end
-
-
[head, filename, content_type, name, body]
-
end
-
-
1
def get_filename(head)
-
filename = nil
-
case head
-
when RFC2183
-
filename = Hash[head.scan(DISPPARM)]['filename']
-
filename = $1 if filename and filename =~ /^"(.*)"$/
-
when BROKEN_QUOTED, BROKEN_UNQUOTED
-
filename = $1
-
end
-
-
return unless filename
-
-
if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
-
filename = Utils.unescape(filename)
-
end
-
-
scrub_filename filename
-
-
if filename !~ /\\[^\\"]/
-
filename = filename.gsub(/\\(.)/, '\1')
-
end
-
filename
-
end
-
-
1
if "<3".respond_to? :valid_encoding?
-
1
def scrub_filename(filename)
-
unless filename.valid_encoding?
-
# FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
-
# We can remove it after they are dropped
-
filename.force_encoding(Encoding::ASCII_8BIT)
-
filename.encode!(:invalid => :replace, :undef => :replace)
-
end
-
end
-
-
1
CHARSET = "charset"
-
1
TEXT_PLAIN = "text/plain"
-
-
1
def tag_multipart_encoding(filename, content_type, name, body)
-
name.force_encoding Encoding::UTF_8
-
-
return if filename
-
-
encoding = Encoding::UTF_8
-
-
if content_type
-
list = content_type.split(';')
-
type_subtype = list.first
-
type_subtype.strip!
-
if TEXT_PLAIN == type_subtype
-
rest = list.drop 1
-
rest.each do |param|
-
k,v = param.split('=', 2)
-
k.strip!
-
v.strip!
-
encoding = Encoding.find v if k == CHARSET
-
end
-
end
-
end
-
-
name.force_encoding encoding
-
body.force_encoding encoding
-
end
-
else
-
def scrub_filename(filename)
-
end
-
def tag_multipart_encoding(filename, content_type, name, body)
-
end
-
end
-
-
1
def get_data(filename, body, content_type, name, head)
-
data = body
-
if filename == ""
-
# filename is blank which means no file has been selected
-
return
-
elsif filename
-
body.rewind if body.respond_to?(:rewind)
-
-
# Take the basename of the upload's original filename.
-
# This handles the full Windows paths given by Internet Explorer
-
# (and perhaps other broken user agents) without affecting
-
# those which give the lone filename.
-
filename = filename.split(/[\/\\]/).last
-
-
data = {:filename => filename, :type => content_type,
-
:name => name, :tempfile => body, :head => head}
-
elsif !filename && content_type && body.is_a?(IO)
-
body.rewind
-
-
# Generic multipart cases, not coming from a form
-
data = {:type => content_type,
-
:name => name, :tempfile => body, :head => head}
-
end
-
-
yield data
-
end
-
end
-
end
-
end
-
1
module Rack
-
1
class NullLogger
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
71
env['rack.logger'] = self
-
71
@app.call(env)
-
end
-
-
1
def info(progname = nil, &block); end
-
1
def debug(progname = nil, &block); end
-
1
def warn(progname = nil, &block); end
-
1
def error(progname = nil, &block); end
-
1
def fatal(progname = nil, &block); end
-
1
def unknown(progname = nil, &block); end
-
1
def info? ; end
-
1
def debug? ; end
-
1
def warn? ; end
-
1
def error? ; end
-
1
def fatal? ; end
-
1
def level ; end
-
1
def progname ; end
-
1
def datetime_format ; end
-
1
def formatter ; end
-
1
def sev_threshold ; end
-
1
def level=(level); end
-
1
def progname=(progname); end
-
1
def datetime_format=(datetime_format); end
-
1
def formatter=(formatter); end
-
1
def sev_threshold=(sev_threshold); end
-
1
def close ; end
-
1
def add(severity, message = nil, progname = nil, &block); end
-
1
def <<(msg); end
-
end
-
end
-
1
require 'rack/utils'
-
-
1
module Rack
-
# Rack::Request provides a convenient interface to a Rack
-
# environment. It is stateless, the environment +env+ passed to the
-
# constructor will be directly modified.
-
#
-
# req = Rack::Request.new(env)
-
# req.post?
-
# req.params["data"]
-
-
1
class Request
-
# The environment of the request.
-
1
attr_reader :env
-
-
1
def initialize(env)
-
344
@env = env
-
end
-
-
1
def body; @env["rack.input"] end
-
1
def script_name; @env[SCRIPT_NAME].to_s end
-
257
def path_info; @env[PATH_INFO].to_s end
-
171
def request_method; @env["REQUEST_METHOD"] end
-
229
def query_string; @env[QUERY_STRING].to_s end
-
1
def content_length; @env['CONTENT_LENGTH'] end
-
-
1
def content_type
-
142
content_type = @env['CONTENT_TYPE']
-
142
content_type.nil? || content_type.empty? ? nil : content_type
-
end
-
-
50
def session; @env['rack.session'] ||= {} end
-
1
def session_options; @env['rack.session.options'] ||= {} end
-
1
def logger; @env['rack.logger'] end
-
-
# The media type (type/subtype) portion of the CONTENT_TYPE header
-
# without any media type parameters. e.g., when CONTENT_TYPE is
-
# "text/plain;charset=utf-8", the media-type is "text/plain".
-
#
-
# For more information on the use of media types in HTTP, see:
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
-
1
def media_type
-
127
content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
-
end
-
-
# The media type parameters provided in CONTENT_TYPE as a Hash, or
-
# an empty Hash if no CONTENT_TYPE or media-type parameters were
-
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
-
# this method responds with the following Hash:
-
# { 'charset' => 'utf-8' }
-
1
def media_type_params
-
return {} if content_type.nil?
-
Hash[*content_type.split(/\s*[;,]\s*/)[1..-1].
-
collect { |s| s.split('=', 2) }.
-
map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten]
-
end
-
-
# The character set of the request body if a "charset" media type
-
# parameter was given, or nil if no "charset" was specified. Note
-
# that, per RFC2616, text/* media types that specify no explicit
-
# charset are to be considered ISO-8859-1.
-
1
def content_charset
-
media_type_params['charset']
-
end
-
-
1
def scheme
-
58
if @env['HTTPS'] == 'on'
-
'https'
-
58
elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
-
'https'
-
58
elsif @env['HTTP_X_FORWARDED_SCHEME']
-
@env['HTTP_X_FORWARDED_SCHEME']
-
58
elsif @env['HTTP_X_FORWARDED_PROTO']
-
@env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
-
else
-
58
@env["rack.url_scheme"]
-
end
-
end
-
-
1
def ssl?
-
28
scheme == 'https'
-
end
-
-
1
def host_with_port
-
88
if forwarded = @env["HTTP_X_FORWARDED_HOST"]
-
forwarded.split(/,\s?/).last
-
else
-
88
@env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
-
end
-
end
-
-
1
def port
-
44
if port = host_with_port.split(/:/)[1]
-
44
port.to_i
-
elsif port = @env['HTTP_X_FORWARDED_PORT']
-
port.to_i
-
elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
-
DEFAULT_PORTS[scheme]
-
elsif @env.has_key?("HTTP_X_FORWARDED_PROTO")
-
DEFAULT_PORTS[@env['HTTP_X_FORWARDED_PROTO'].split(',')[0]]
-
else
-
@env["SERVER_PORT"].to_i
-
end
-
end
-
-
1
def host
-
# Remove port number.
-
30
host_with_port.to_s.sub(/:\d+\z/, '')
-
end
-
-
1
def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
-
1
def path_info=(s); @env["PATH_INFO"] = s.to_s end
-
-
-
# Checks the HTTP request method (or verb) to see if it was of type DELETE
-
1
def delete?; request_method == "DELETE" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type GET
-
1
def get?; request_method == GET end
-
-
# Checks the HTTP request method (or verb) to see if it was of type HEAD
-
72
def head?; request_method == HEAD end
-
-
# Checks the HTTP request method (or verb) to see if it was of type OPTIONS
-
1
def options?; request_method == "OPTIONS" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type LINK
-
1
def link?; request_method == "LINK" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type PATCH
-
1
def patch?; request_method == "PATCH" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type POST
-
1
def post?; request_method == "POST" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type PUT
-
1
def put?; request_method == "PUT" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type TRACE
-
1
def trace?; request_method == "TRACE" end
-
-
# Checks the HTTP request method (or verb) to see if it was of type UNLINK
-
1
def unlink?; request_method == "UNLINK" end
-
-
-
# The set of form-data media-types. Requests that do not indicate
-
# one of the media types presents in this list will not be eligible
-
# for form-data / param parsing.
-
1
FORM_DATA_MEDIA_TYPES = [
-
'application/x-www-form-urlencoded',
-
'multipart/form-data'
-
]
-
-
# The set of media-types. Requests that do not indicate
-
# one of the media types presents in this list will not be eligible
-
# for param parsing like soap attachments or generic multiparts
-
1
PARSEABLE_DATA_MEDIA_TYPES = [
-
'multipart/related',
-
'multipart/mixed'
-
]
-
-
# Default ports depending on scheme. Used to decide whether or not
-
# to include the port in a generated URI.
-
1
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
-
-
# Determine whether the request body contains form-data by checking
-
# the request Content-Type for one of the media-types:
-
# "application/x-www-form-urlencoded" or "multipart/form-data". The
-
# list of form-data media types can be modified through the
-
# +FORM_DATA_MEDIA_TYPES+ array.
-
#
-
# A request body is also assumed to contain form-data when no
-
# Content-Type header is provided and the request_method is POST.
-
1
def form_data?
-
71
type = media_type
-
71
meth = env["rack.methodoverride.original_method"] || env[REQUEST_METHOD]
-
71
(meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
-
end
-
-
# Determine whether the request body contains data by checking
-
# the request media_type against registered parse-data media-types
-
1
def parseable_data?
-
56
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
-
end
-
-
# Returns the data received in the query string.
-
1
def GET
-
86
if @env["rack.request.query_string"] == query_string
-
15
@env["rack.request.query_hash"]
-
else
-
71
p = parse_query({ :query => query_string, :separator => '&;' })
-
71
@env["rack.request.query_string"] = query_string
-
71
@env["rack.request.query_hash"] = p
-
end
-
end
-
-
# Returns the data received in the request body.
-
#
-
# This method support both application/x-www-form-urlencoded and
-
# multipart/form-data.
-
1
def POST
-
101
if @env["rack.input"].nil?
-
raise "Missing rack.input"
-
101
elsif @env["rack.request.form_input"].equal? @env["rack.input"]
-
30
@env["rack.request.form_hash"]
-
71
elsif form_data? || parseable_data?
-
15
unless @env["rack.request.form_hash"] = parse_multipart(env)
-
15
form_vars = @env["rack.input"].read
-
-
# Fix for Safari Ajax postings that always append \0
-
# form_vars.sub!(/\0\z/, '') # performance replacement:
-
15
form_vars.slice!(-1) if form_vars[-1] == ?\0
-
-
15
@env["rack.request.form_vars"] = form_vars
-
15
@env["rack.request.form_hash"] = parse_query({ :query => form_vars, :separator => '&' })
-
-
15
@env["rack.input"].rewind
-
end
-
15
@env["rack.request.form_input"] = @env["rack.input"]
-
15
@env["rack.request.form_hash"]
-
else
-
56
{}
-
end
-
end
-
-
# The union of GET and POST data.
-
#
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
-
1
def params
-
86
@params ||= self.GET.merge(self.POST)
-
rescue EOFError
-
self.GET.dup
-
end
-
-
# Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
-
#
-
# The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
-
#
-
# env['rack.input'] is not touched.
-
1
def update_param(k, v)
-
found = false
-
if self.GET.has_key?(k)
-
found = true
-
self.GET[k] = v
-
end
-
if self.POST.has_key?(k)
-
found = true
-
self.POST[k] = v
-
end
-
unless found
-
self.GET[k] = v
-
end
-
@params = nil
-
nil
-
end
-
-
# Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
-
#
-
# If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
-
#
-
# env['rack.input'] is not touched.
-
1
def delete_param(k)
-
v = [ self.POST.delete(k), self.GET.delete(k) ].compact.first
-
@params = nil
-
v
-
end
-
-
# shortcut for request.params[key]
-
1
def [](key)
-
params[key.to_s]
-
end
-
-
# shortcut for request.params[key] = value
-
#
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
-
1
def []=(key, value)
-
params[key.to_s] = value
-
end
-
-
# like Hash#values_at
-
1
def values_at(*keys)
-
keys.map{|key| params[key] }
-
end
-
-
# the referer of the client
-
1
def referer
-
@env['HTTP_REFERER']
-
end
-
1
alias referrer referer
-
-
1
def user_agent
-
@env['HTTP_USER_AGENT']
-
end
-
-
1
def cookies
-
142
hash = @env["rack.request.cookie_hash"] ||= {}
-
142
string = @env["HTTP_COOKIE"]
-
-
142
return hash if string == @env["rack.request.cookie_string"]
-
62
hash.clear
-
-
# According to RFC 2109:
-
# If multiple cookies satisfy the criteria above, they are ordered in
-
# the Cookie header such that those with more specific Path attributes
-
# precede those with less specific. Ordering with respect to other
-
# attributes (e.g., Domain) is unspecified.
-
186
cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
-
124
cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
-
62
@env["rack.request.cookie_string"] = string
-
62
hash
-
end
-
-
1
def xhr?
-
71
@env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
-
end
-
-
1
def base_url
-
url = "#{scheme}://#{host}"
-
url << ":#{port}" if port != DEFAULT_PORTS[scheme]
-
url
-
end
-
-
# Tries to return a remake of the original request URL as a string.
-
1
def url
-
base_url + fullpath
-
end
-
-
1
def path
-
script_name + path_info
-
end
-
-
1
def fullpath
-
query_string.empty? ? path : "#{path}?#{query_string}"
-
end
-
-
1
def accept_encoding
-
parse_http_accept_header(@env["HTTP_ACCEPT_ENCODING"])
-
end
-
-
1
def accept_language
-
parse_http_accept_header(@env["HTTP_ACCEPT_LANGUAGE"])
-
end
-
-
1
def trusted_proxy?(ip)
-
ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
-
end
-
-
1
def ip
-
remote_addrs = split_ip_addresses(@env['REMOTE_ADDR'])
-
remote_addrs = reject_trusted_ip_addresses(remote_addrs)
-
-
return remote_addrs.first if remote_addrs.any?
-
-
forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR'])
-
-
return reject_trusted_ip_addresses(forwarded_ips).last || @env["REMOTE_ADDR"]
-
end
-
-
1
protected
-
1
def split_ip_addresses(ip_addresses)
-
ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
-
end
-
-
1
def reject_trusted_ip_addresses(ip_addresses)
-
ip_addresses.reject { |ip| trusted_proxy?(ip) }
-
end
-
-
1
def parse_query(qs)
-
86
d = '&'
-
86
qs, d = qs[:query], qs[:separator] if Hash === qs
-
86
Utils.parse_nested_query(qs, d)
-
end
-
-
1
def parse_multipart(env)
-
15
Rack::Multipart.parse_multipart(env)
-
end
-
-
1
def parse_http_accept_header(header)
-
header.to_s.split(/\s*,\s*/).map do |part|
-
attribute, parameters = part.split(/\s*;\s*/, 2)
-
quality = 1.0
-
if parameters and /\Aq=([\d.]+)/ =~ parameters
-
quality = $1.to_f
-
end
-
[attribute, quality]
-
end
-
end
-
-
1
private
-
1
def strip_doublequotes(s)
-
if s[0] == ?" && s[-1] == ?"
-
s[1..-2]
-
else
-
s
-
end
-
end
-
end
-
end
-
1
require 'rack/request'
-
1
require 'rack/utils'
-
1
require 'rack/body_proxy'
-
1
require 'time'
-
-
1
module Rack
-
# Rack::Response provides a convenient interface to create a Rack
-
# response.
-
#
-
# It allows setting of headers and cookies, and provides useful
-
# defaults (a OK response containing HTML).
-
#
-
# You can use Response#write to iteratively generate your response,
-
# but note that this is buffered by Rack::Response until you call
-
# +finish+. +finish+ however can take a block inside which calls to
-
# +write+ are synchronous with the Rack response.
-
#
-
# Your application's +call+ should end returning Response#finish.
-
-
1
class Response
-
1
attr_accessor :length
-
-
1
CHUNKED = 'chunked'.freeze
-
1
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
-
1
def initialize(body=[], status=200, header={})
-
71
@status = status.to_i
-
71
@header = Utils::HeaderHash.new.merge(header)
-
-
71
@chunked = CHUNKED == @header[TRANSFER_ENCODING]
-
71
@writer = lambda { |x| @body << x }
-
71
@block = nil
-
71
@length = 0
-
-
71
@body = []
-
-
71
if body.respond_to? :to_str
-
write body.to_str
-
71
elsif body.respond_to?(:each)
-
71
body.each { |part|
-
write part.to_s
-
}
-
else
-
raise TypeError, "stringable or iterable required"
-
end
-
-
71
yield self if block_given?
-
end
-
-
1
attr_reader :header
-
1
attr_accessor :status, :body
-
-
1
def [](key)
-
71
header[key]
-
end
-
-
1
def []=(key, value)
-
156
header[key] = value
-
end
-
-
1
def set_cookie(key, value)
-
Utils.set_cookie_header!(header, key, value)
-
end
-
-
1
def delete_cookie(key, value={})
-
Utils.delete_cookie_header!(header, key, value)
-
end
-
-
1
def redirect(target, status=302)
-
self.status = status
-
self["Location"] = target
-
end
-
-
1
def finish(&block)
-
@block = block
-
-
if [204, 205, 304].include?(status.to_i)
-
header.delete CONTENT_TYPE
-
header.delete CONTENT_LENGTH
-
close
-
[status.to_i, header, []]
-
else
-
[status.to_i, header, BodyProxy.new(self){}]
-
end
-
end
-
1
alias to_a finish # For *response
-
1
alias to_ary finish # For implicit-splat on Ruby 1.9.2
-
-
1
def each(&callback)
-
@body.each(&callback)
-
@writer = callback
-
@block.call(self) if @block
-
end
-
-
# Append to body and update Content-Length.
-
#
-
# NOTE: Do not mix #write and direct #body access!
-
#
-
1
def write(str)
-
s = str.to_s
-
@length += Rack::Utils.bytesize(s) unless @chunked
-
@writer.call s
-
-
header[CONTENT_LENGTH] = @length.to_s unless @chunked
-
str
-
end
-
-
1
def close
-
body.close if body.respond_to?(:close)
-
end
-
-
1
def empty?
-
@block == nil && @body.empty?
-
end
-
-
1
alias headers header
-
-
1
module Helpers
-
1
def invalid?; status < 100 || status >= 600; end
-
-
1
def informational?; status >= 100 && status < 200; end
-
1
def successful?; status >= 200 && status < 300; end
-
1
def redirection?; status >= 300 && status < 400; end
-
1
def client_error?; status >= 400 && status < 500; end
-
1
def server_error?; status >= 500 && status < 600; end
-
-
1
def ok?; status == 200; end
-
1
def created?; status == 201; end
-
1
def accepted?; status == 202; end
-
1
def bad_request?; status == 400; end
-
1
def unauthorized?; status == 401; end
-
1
def forbidden?; status == 403; end
-
1
def not_found?; status == 404; end
-
1
def method_not_allowed?; status == 405; end
-
1
def i_m_a_teapot?; status == 418; end
-
1
def unprocessable?; status == 422; end
-
-
1
def redirect?; [301, 302, 303, 307].include? status; end
-
-
# Headers
-
1
attr_reader :headers, :original_headers
-
-
1
def include?(header)
-
!!headers[header]
-
end
-
-
1
def content_type
-
headers[CONTENT_TYPE]
-
end
-
-
1
def content_length
-
cl = headers[CONTENT_LENGTH]
-
cl ? cl.to_i : cl
-
end
-
-
1
def location
-
headers["Location"]
-
end
-
end
-
-
1
include Helpers
-
end
-
end
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-
# bugrep: Andreas Zehnder
-
-
1
require 'time'
-
1
require 'rack/request'
-
1
require 'rack/response'
-
1
begin
-
1
require 'securerandom'
-
rescue LoadError
-
# We just won't get securerandom
-
end
-
-
1
module Rack
-
-
1
module Session
-
-
1
module Abstract
-
1
ENV_SESSION_KEY = 'rack.session'.freeze
-
1
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
-
-
# SessionHash is responsible to lazily load the session from store.
-
-
1
class SessionHash
-
1
include Enumerable
-
1
attr_writer :id
-
-
1
def self.find(env)
-
env[ENV_SESSION_KEY]
-
end
-
-
1
def self.set(env, session)
-
env[ENV_SESSION_KEY] = session
-
end
-
-
1
def self.set_options(env, options)
-
env[ENV_SESSION_OPTIONS_KEY] = options.dup
-
end
-
-
1
def initialize(store, env)
-
71
@store = store
-
71
@env = env
-
71
@loaded = false
-
end
-
-
1
def id
-
213
return @id if @loaded or instance_variable_defined?(:@id)
-
71
@id = @store.send(:extract_session_id, @env)
-
end
-
-
1
def options
-
71
@env[ENV_SESSION_OPTIONS_KEY]
-
end
-
-
1
def each(&block)
-
load_for_read!
-
@data.each(&block)
-
end
-
-
1
def [](key)
-
191
load_for_read!
-
191
@data[key.to_s]
-
end
-
1
alias :fetch :[]
-
-
1
def has_key?(key)
-
71
load_for_read!
-
71
@data.has_key?(key.to_s)
-
end
-
1
alias :key? :has_key?
-
1
alias :include? :has_key?
-
-
1
def []=(key, value)
-
34
load_for_write!
-
34
@data[key.to_s] = value
-
end
-
1
alias :store :[]=
-
-
1
def clear
-
load_for_write!
-
@data.clear
-
end
-
-
1
def destroy
-
clear
-
@id = @store.send(:destroy_session, @env, id, options)
-
end
-
-
1
def to_hash
-
71
load_for_read!
-
71
@data.dup
-
end
-
-
1
def update(hash)
-
load_for_write!
-
@data.update(stringify_keys(hash))
-
end
-
1
alias :merge! :update
-
-
1
def replace(hash)
-
load_for_write!
-
@data.replace(stringify_keys(hash))
-
end
-
-
1
def delete(key)
-
load_for_write!
-
@data.delete(key.to_s)
-
end
-
-
1
def inspect
-
if loaded?
-
@data.inspect
-
else
-
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
-
end
-
end
-
-
1
def exists?
-
80
return @exists if instance_variable_defined?(:@exists)
-
71
@data = {}
-
71
@exists = @store.send(:session_exists?, @env)
-
end
-
-
1
def loaded?
-
509
@loaded
-
end
-
-
1
def empty?
-
load_for_read!
-
@data.empty?
-
end
-
-
1
def keys
-
@data.keys
-
end
-
-
1
def values
-
@data.values
-
end
-
-
1
private
-
-
1
def load_for_read!
-
333
load! if !loaded? && exists?
-
end
-
-
1
def load_for_write!
-
34
load! unless loaded?
-
end
-
-
1
def load!
-
71
@id, session = @store.send(:load_session, @env)
-
71
@data = stringify_keys(session)
-
71
@loaded = true
-
end
-
-
1
def stringify_keys(other)
-
71
hash = {}
-
71
other.each do |key, value|
-
231
hash[key.to_s] = value
-
end
-
71
hash
-
end
-
end
-
-
# ID sets up a basic framework for implementing an id based sessioning
-
# service. Cookies sent to the client for maintaining sessions will only
-
# contain an id reference. Only #get_session and #set_session are
-
# required to be overwritten.
-
#
-
# All parameters are optional.
-
# * :key determines the name of the cookie, by default it is
-
# 'rack.session'
-
# * :path, :domain, :expire_after, :secure, and :httponly set the related
-
# cookie options as by Rack::Response#add_cookie
-
# * :skip will not a set a cookie in the response nor update the session state
-
# * :defer will not set a cookie in the response but still update the session
-
# state if it is used with a backend
-
# * :renew (implementation dependent) will prompt the generation of a new
-
# session id, and migration of data to be referenced at the new id. If
-
# :defer is set, it will be overridden and the cookie will be set.
-
# * :sidbits sets the number of bits in length that a generated session
-
# id will be.
-
#
-
# These options can be set on a per request basis, at the location of
-
# env['rack.session.options']. Additionally the id of the session can be
-
# found within the options hash at the key :id. It is highly not
-
# recommended to change its value.
-
#
-
# Is Rack::Utils::Context compatible.
-
#
-
# Not included by default; you must require 'rack/session/abstract/id'
-
# to use.
-
-
1
class ID
-
1
DEFAULT_OPTIONS = {
-
:key => 'rack.session',
-
:path => '/',
-
:domain => nil,
-
:expire_after => nil,
-
:secure => false,
-
:httponly => true,
-
:defer => false,
-
:renew => false,
-
:sidbits => 128,
-
:cookie_only => true,
-
1
:secure_random => (::SecureRandom rescue false)
-
}
-
-
1
attr_reader :key, :default_options
-
-
1
def initialize(app, options={})
-
1
@app = app
-
1
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
-
1
@key = @default_options.delete(:key)
-
1
@cookie_only = @default_options.delete(:cookie_only)
-
1
initialize_sid
-
end
-
-
1
def call(env)
-
71
context(env)
-
end
-
-
1
def context(env, app=@app)
-
71
prepare_session(env)
-
71
status, headers, body = app.call(env)
-
71
commit_session(env, status, headers, body)
-
end
-
-
1
private
-
-
1
def initialize_sid
-
1
@sidbits = @default_options[:sidbits]
-
1
@sid_secure = @default_options[:secure_random]
-
1
@sid_length = @sidbits / 4
-
end
-
-
# Generate a new session id using Ruby #rand. The size of the
-
# session id is controlled by the :sidbits option.
-
# Monkey patch this to use custom methods for session id generation.
-
-
1
def generate_sid(secure = @sid_secure)
-
9
if secure
-
9
secure.hex(@sid_length)
-
else
-
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
-
end
-
rescue NotImplementedError
-
generate_sid(false)
-
end
-
-
# Sets the lazy session at 'rack.session' and places options and session
-
# metadata into 'rack.session.options'.
-
-
1
def prepare_session(env)
-
71
session_was = env[ENV_SESSION_KEY]
-
71
env[ENV_SESSION_KEY] = session_class.new(self, env)
-
71
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
-
71
env[ENV_SESSION_KEY].merge! session_was if session_was
-
end
-
-
# Extracts the session id from provided cookies and passes it and the
-
# environment to #get_session.
-
-
1
def load_session(env)
-
71
sid = current_session_id(env)
-
71
sid, session = get_session(env, sid)
-
71
[sid, session || {}]
-
end
-
-
# Extract session id from request object.
-
-
1
def extract_session_id(env)
-
request = Rack::Request.new(env)
-
sid = request.cookies[@key]
-
sid ||= request.params[@key] unless @cookie_only
-
sid
-
end
-
-
# Returns the current session id from the SessionHash.
-
-
1
def current_session_id(env)
-
142
env[ENV_SESSION_KEY].id
-
end
-
-
# Check if the session exists or not.
-
-
1
def session_exists?(env)
-
71
value = current_session_id(env)
-
71
value && !value.empty?
-
end
-
-
# Session should be committed if it was loaded, any of specific options like :renew, :drop
-
# or :expire_after was given and the security permissions match. Skips if skip is given.
-
-
1
def commit_session?(env, session, options)
-
71
if options[:skip]
-
false
-
else
-
71
has_session = loaded_session?(session) || forced_session_update?(session, options)
-
71
has_session && security_matches?(env, options)
-
end
-
end
-
-
1
def loaded_session?(session)
-
142
!session.is_a?(session_class) || session.loaded?
-
end
-
-
1
def forced_session_update?(session, options)
-
force_options?(options) && session && !session.empty?
-
end
-
-
1
def force_options?(options)
-
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
-
end
-
-
1
def security_matches?(env, options)
-
71
return true unless options[:secure]
-
request = Rack::Request.new(env)
-
request.ssl?
-
end
-
-
# Acquires the session from the environment and the session id from
-
# the session options and passes them to #set_session. If successful
-
# and the :defer option is not true, a cookie will be added to the
-
# response with the session's id.
-
-
1
def commit_session(env, status, headers, body)
-
71
session = env[ENV_SESSION_KEY]
-
71
options = session.options
-
-
71
if options[:drop] || options[:renew]
-
session_id = destroy_session(env, session.id || generate_sid, options)
-
return [status, headers, body] unless session_id
-
end
-
-
71
return [status, headers, body] unless commit_session?(env, session, options)
-
-
71
session.send(:load!) unless loaded_session?(session)
-
71
session_id ||= session.id
-
332
session_data = session.to_hash.delete_if { |k,v| v.nil? }
-
-
71
if not data = set_session(env, session_id, session_data, options)
-
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
-
71
elsif options[:defer] and not options[:renew]
-
env["rack.errors"].puts("Deferring cookie for #{session_id}") if $VERBOSE
-
else
-
71
cookie = Hash.new
-
71
cookie[:value] = data
-
71
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
-
71
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
-
71
set_cookie(env, headers, cookie.merge!(options))
-
end
-
-
71
[status, headers, body]
-
end
-
-
# Sets the cookie back to the client with session id. We skip the cookie
-
# setting if the value didn't change (sid is the same) or expires was given.
-
-
1
def set_cookie(env, headers, cookie)
-
71
request = Rack::Request.new(env)
-
71
if request.cookies[@key] != cookie[:value] || cookie[:expires]
-
21
Utils.set_cookie_header!(headers, @key, cookie)
-
end
-
end
-
-
# Allow subclasses to prepare_session for different Session classes
-
-
1
def session_class
-
213
SessionHash
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
1
def get_session(env, sid)
-
raise '#get_session not implemented.'
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
1
def set_session(env, sid, session, options)
-
raise '#set_session not implemented.'
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
1
def destroy_session(env, sid, options)
-
raise '#destroy_session not implemented'
-
end
-
end
-
end
-
end
-
end
-
1
require 'openssl'
-
1
require 'zlib'
-
1
require 'rack/request'
-
1
require 'rack/response'
-
1
require 'rack/session/abstract/id'
-
-
1
module Rack
-
-
1
module Session
-
-
# Rack::Session::Cookie provides simple cookie based session management.
-
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
-
# data set to :key (default: rack.session). The object that encodes the
-
# session data is configurable and must respond to +encode+ and +decode+.
-
# Both methods must take a string and return a string.
-
#
-
# When the secret key is set, cookie data is checked for data integrity.
-
# The old secret key is also accepted and allows graceful secret rotation.
-
#
-
# Example:
-
#
-
# use Rack::Session::Cookie, :key => 'rack.session',
-
# :domain => 'foo.com',
-
# :path => '/',
-
# :expire_after => 2592000,
-
# :secret => 'change_me',
-
# :old_secret => 'also_change_me'
-
#
-
# All parameters are optional.
-
#
-
# Example of a cookie with no encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Rack::Session::Cookie::Identity.new
-
# })
-
#
-
# Example of a cookie with custom encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Class.new {
-
# def encode(str); str.reverse; end
-
# def decode(str); str.reverse; end
-
# }.new
-
# })
-
#
-
-
1
class Cookie < Abstract::ID
-
# Encode session cookies as Base64
-
1
class Base64
-
1
def encode(str)
-
71
[str].pack('m')
-
end
-
-
1
def decode(str)
-
62
str.unpack('m').first
-
end
-
-
# Encode session cookies as Marshaled Base64 data
-
1
class Marshal < Base64
-
1
def encode(str)
-
71
super(::Marshal.dump(str))
-
end
-
-
1
def decode(str)
-
71
return unless str
-
62
::Marshal.load(super(str)) rescue nil
-
end
-
end
-
-
# N.B. Unlike other encoding methods, the contained objects must be a
-
# valid JSON composite type, either a Hash or an Array.
-
1
class JSON < Base64
-
1
def encode(obj)
-
super(::Rack::Utils::OkJson.encode(obj))
-
end
-
-
1
def decode(str)
-
return unless str
-
::Rack::Utils::OkJson.decode(super(str)) rescue nil
-
end
-
end
-
-
1
class ZipJSON < Base64
-
1
def encode(obj)
-
super(Zlib::Deflate.deflate(::Rack::Utils::OkJson.encode(obj)))
-
end
-
-
1
def decode(str)
-
return unless str
-
::Rack::Utils::OkJson.decode(Zlib::Inflate.inflate(super(str)))
-
rescue
-
nil
-
end
-
end
-
end
-
-
# Use no encoding for session cookies
-
1
class Identity
-
1
def encode(str); str; end
-
1
def decode(str); str; end
-
end
-
-
1
attr_reader :coder
-
-
1
def initialize(app, options={})
-
1
@secrets = options.values_at(:secret, :old_secret).compact
-
warn <<-MSG unless @secrets.size >= 1
-
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
-
This poses a security threat. It is strongly recommended that you
-
provide a secret to prevent exploits that may be possible from crafted
-
cookies. This will not be supported in future versions of Rack, and
-
future versions will even invalidate your existing user cookies.
-
-
Called from: #{caller[0]}.
-
1
MSG
-
1
@coder = options[:coder] ||= Base64::Marshal.new
-
1
super(app, options.merge!(:cookie_only => true))
-
end
-
-
1
private
-
-
1
def get_session(env, sid)
-
71
data = unpacked_cookie_data(env)
-
71
data = persistent_session_id!(data)
-
71
[data["session_id"], data]
-
end
-
-
1
def extract_session_id(env)
-
71
unpacked_cookie_data(env)["session_id"]
-
end
-
-
1
def unpacked_cookie_data(env)
-
142
env["rack.session.unpacked_cookie_data"] ||= begin
-
71
request = Rack::Request.new(env)
-
71
session_data = request.cookies[@key]
-
-
71
if @secrets.size > 0 && session_data
-
62
digest, session_data = session_data.reverse.split("--", 2)
-
62
digest.reverse! if digest
-
62
session_data.reverse! if session_data
-
62
session_data = nil unless digest_match?(session_data, digest)
-
end
-
-
71
coder.decode(session_data) || {}
-
end
-
end
-
-
1
def persistent_session_id!(data, sid=nil)
-
71
data ||= {}
-
71
data["session_id"] ||= sid || generate_sid
-
71
data
-
end
-
-
1
def set_session(env, session_id, session, options)
-
71
session = session.merge("session_id" => session_id)
-
71
session_data = coder.encode(session)
-
-
71
if @secrets.first
-
71
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
-
end
-
-
71
if session_data.size > (4096 - @key.size)
-
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
-
nil
-
else
-
71
session_data
-
end
-
end
-
-
1
def destroy_session(env, session_id, options)
-
# Nothing to do here, data is in the client
-
generate_sid unless options[:drop]
-
end
-
-
1
def digest_match?(data, digest)
-
62
return unless data && digest
-
62
@secrets.any? do |secret|
-
62
Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
-
end
-
end
-
-
1
def generate_hmac(data, secret)
-
133
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
-
end
-
-
end
-
end
-
end
-
1
require 'ostruct'
-
1
require 'erb'
-
1
require 'rack/request'
-
1
require 'rack/utils'
-
-
1
module Rack
-
# Rack::ShowExceptions catches all exceptions raised from the app it
-
# wraps. It shows a useful backtrace with the sourcefile and
-
# clickable context, the whole Rack environment and the request
-
# data.
-
#
-
# Be careful when you use this on public-facing sites as it could
-
# reveal information helpful to attackers.
-
-
1
class ShowExceptions
-
1
CONTEXT = 7
-
-
1
def initialize(app)
-
@app = app
-
@template = ERB.new(TEMPLATE)
-
end
-
-
1
def call(env)
-
@app.call(env)
-
rescue StandardError, LoadError, SyntaxError => e
-
exception_string = dump_exception(e)
-
-
env["rack.errors"].puts(exception_string)
-
env["rack.errors"].flush
-
-
if accepts_html?(env)
-
content_type = "text/html"
-
body = pretty(env, e)
-
else
-
content_type = "text/plain"
-
body = exception_string
-
end
-
-
[
-
500,
-
{
-
CONTENT_TYPE => content_type,
-
CONTENT_LENGTH => Rack::Utils.bytesize(body).to_s,
-
},
-
[body],
-
]
-
end
-
-
1
def prefers_plaintext?(env)
-
!accepts_html(env)
-
end
-
-
1
def accepts_html?(env)
-
Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html])
-
end
-
1
private :accepts_html?
-
-
1
def dump_exception(exception)
-
string = "#{exception.class}: #{exception.message}\n"
-
string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
-
string
-
end
-
-
1
def pretty(env, exception)
-
req = Rack::Request.new(env)
-
-
# This double assignment is to prevent an "unused variable" warning on
-
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
-
path = path = (req.script_name + req.path_info).squeeze("/")
-
-
# This double assignment is to prevent an "unused variable" warning on
-
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
-
frames = frames = exception.backtrace.map { |line|
-
frame = OpenStruct.new
-
if line =~ /(.*?):(\d+)(:in `(.*)')?/
-
frame.filename = $1
-
frame.lineno = $2.to_i
-
frame.function = $4
-
-
begin
-
lineno = frame.lineno-1
-
lines = ::File.readlines(frame.filename)
-
frame.pre_context_lineno = [lineno-CONTEXT, 0].max
-
frame.pre_context = lines[frame.pre_context_lineno...lineno]
-
frame.context_line = lines[lineno].chomp
-
frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
-
frame.post_context = lines[lineno+1..frame.post_context_lineno]
-
rescue
-
end
-
-
frame
-
else
-
nil
-
end
-
}.compact
-
-
@template.result(binding)
-
end
-
-
1
def h(obj) # :nodoc:
-
case obj
-
when String
-
Utils.escape_html(obj)
-
else
-
Utils.escape_html(obj.inspect)
-
end
-
end
-
-
# :stopdoc:
-
-
# adapted from Django <djangoproject.com>
-
# Copyright (c) 2005, the Lawrence Journal-World
-
# Used under the modified BSD license:
-
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
-
1
TEMPLATE = <<'HTML'
-
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
<html lang="en">
-
<head>
-
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
-
<meta name="robots" content="NONE,NOARCHIVE" />
-
<title><%=h exception.class %> at <%=h path %></title>
-
<style type="text/css">
-
html * { padding:0; margin:0; }
-
body * { padding:10px 20px; }
-
body * * { padding:0; }
-
body { font:small sans-serif; }
-
body>div { border-bottom:1px solid #ddd; }
-
h1 { font-weight:normal; }
-
h2 { margin-bottom:.8em; }
-
h2 span { font-size:80%; color:#666; font-weight:normal; }
-
h3 { margin:1em 0 .5em 0; }
-
h4 { margin:0 0 .5em 0; font-weight: normal; }
-
table {
-
border:1px solid #ccc; border-collapse: collapse; background:white; }
-
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
-
thead th {
-
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
-
font-weight:normal; font-size:11px; border:1px solid #ddd; }
-
tbody th { text-align:right; color:#666; padding-right:.5em; }
-
table.vars { margin:5px 0 2px 40px; }
-
table.vars td, table.req td { font-family:monospace; }
-
table td.code { width:100%;}
-
table td.code div { overflow:hidden; }
-
table.source th { color:#666; }
-
table.source td {
-
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
-
ul.traceback { list-style-type:none; }
-
ul.traceback li.frame { margin-bottom:1em; }
-
div.context { margin: 10px 0; }
-
div.context ol {
-
padding-left:30px; margin:0 10px; list-style-position: inside; }
-
div.context ol li {
-
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
-
div.context ol.context-line li { color:black; background-color:#ccc; }
-
div.context ol.context-line li span { float: right; }
-
div.commands { margin-left: 40px; }
-
div.commands a { color:black; text-decoration:none; }
-
#summary { background: #ffc; }
-
#summary h2 { font-weight: normal; color: #666; }
-
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
-
#summary ul#quicklinks li { float: left; padding: 0 1em; }
-
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
-
#explanation { background:#eee; }
-
#template, #template-not-exist { background:#f6f6f6; }
-
#template-not-exist ul { margin: 0 0 0 20px; }
-
#traceback { background:#eee; }
-
#requestinfo { background:#f6f6f6; padding-left:120px; }
-
#summary table { border:none; background:transparent; }
-
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
-
#requestinfo h3 { margin-bottom:-1em; }
-
.error { background: #ffc; }
-
.specific { color:#cc3300; font-weight:bold; }
-
</style>
-
<script type="text/javascript">
-
//<!--
-
function getElementsByClassName(oElm, strTagName, strClassName){
-
// Written by Jonathan Snook, http://www.snook.ca/jon;
-
// Add-ons by Robert Nyman, http://www.robertnyman.com
-
var arrElements = (strTagName == "*" && document.all)? document.all :
-
oElm.getElementsByTagName(strTagName);
-
var arrReturnElements = new Array();
-
strClassName = strClassName.replace(/\-/g, "\\-");
-
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
-
var oElement;
-
for(var i=0; i<arrElements.length; i++){
-
oElement = arrElements[i];
-
if(oRegExp.test(oElement.className)){
-
arrReturnElements.push(oElement);
-
}
-
}
-
return (arrReturnElements)
-
}
-
function hideAll(elems) {
-
for (var e = 0; e < elems.length; e++) {
-
elems[e].style.display = 'none';
-
}
-
}
-
window.onload = function() {
-
hideAll(getElementsByClassName(document, 'table', 'vars'));
-
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
-
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
-
}
-
function toggle() {
-
for (var i = 0; i < arguments.length; i++) {
-
var e = document.getElementById(arguments[i]);
-
if (e) {
-
e.style.display = e.style.display == 'none' ? 'block' : 'none';
-
}
-
}
-
return false;
-
}
-
function varToggle(link, id) {
-
toggle('v' + id);
-
var s = link.getElementsByTagName('span')[0];
-
var uarr = String.fromCharCode(0x25b6);
-
var darr = String.fromCharCode(0x25bc);
-
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
-
return false;
-
}
-
//-->
-
</script>
-
</head>
-
<body>
-
-
<div id="summary">
-
<h1><%=h exception.class %> at <%=h path %></h1>
-
<h2><%=h exception.message %></h2>
-
<table><tr>
-
<th>Ruby</th>
-
<td>
-
<% if first = frames.first %>
-
<code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
-
<% else %>
-
unknown location
-
<% end %>
-
</td>
-
</tr><tr>
-
<th>Web</th>
-
<td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
-
</tr></table>
-
-
<h3>Jump to:</h3>
-
<ul id="quicklinks">
-
<li><a href="#get-info">GET</a></li>
-
<li><a href="#post-info">POST</a></li>
-
<li><a href="#cookie-info">Cookies</a></li>
-
<li><a href="#env-info">ENV</a></li>
-
</ul>
-
</div>
-
-
<div id="traceback">
-
<h2>Traceback <span>(innermost first)</span></h2>
-
<ul class="traceback">
-
<% frames.each { |frame| %>
-
<li class="frame">
-
<code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
-
-
<% if frame.context_line %>
-
<div class="context" id="c<%=h frame.object_id %>">
-
<% if frame.pre_context %>
-
<ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
-
<% frame.pre_context.each { |line| %>
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
-
<% } %>
-
</ol>
-
<% end %>
-
-
<ol start="<%=h frame.lineno %>" class="context-line">
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
-
-
<% if frame.post_context %>
-
<ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
-
<% frame.post_context.each { |line| %>
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
-
<% } %>
-
</ol>
-
<% end %>
-
</div>
-
<% end %>
-
</li>
-
<% } %>
-
</ul>
-
</div>
-
-
<div id="requestinfo">
-
<h2>Request information</h2>
-
-
<h3 id="get-info">GET</h3>
-
<% if req.GET and not req.GET.empty? %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p>No GET data.</p>
-
<% end %>
-
-
<h3 id="post-info">POST</h3>
-
<% if req.POST and not req.POST.empty? %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p>No POST data.</p>
-
<% end %>
-
-
-
<h3 id="cookie-info">COOKIES</h3>
-
<% unless req.cookies.empty? %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.cookies.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p>No cookie data.</p>
-
<% end %>
-
-
<h3 id="env-info">Rack ENV</h3>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
-
</div>
-
-
<div id="explanation">
-
<p>
-
You're seeing this error because you use <code>Rack::ShowExceptions</code>.
-
</p>
-
</div>
-
-
</body>
-
</html>
-
HTML
-
-
# :startdoc:
-
end
-
end
-
# -*- encoding: binary -*-
-
1
require 'fileutils'
-
1
require 'set'
-
1
require 'tempfile'
-
1
require 'rack/multipart'
-
1
require 'time'
-
-
4
major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
-
-
1
if major == 1 && minor < 9
-
require 'rack/backports/uri/common_18'
-
1
elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
-
require 'rack/backports/uri/common_192'
-
1
elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
-
require 'rack/backports/uri/common_193'
-
else
-
1
require 'uri/common'
-
end
-
-
1
module Rack
-
# Rack::Utils contains a grab-bag of useful methods for writing web
-
# applications adopted from all kinds of Ruby libraries.
-
-
1
module Utils
-
# ParameterTypeError is the error that is raised when incoming structural
-
# parameters (parsed by parse_nested_query) contain conflicting types.
-
1
class ParameterTypeError < TypeError; end
-
-
# InvalidParameterError is the error that is raised when incoming structural
-
# parameters (parsed by parse_nested_query) contain invalid format or byte
-
# sequence.
-
1
class InvalidParameterError < ArgumentError; end
-
-
# URI escapes. (CGI style space to +)
-
1
def escape(s)
-
42
URI.encode_www_form_component(s)
-
end
-
1
module_function :escape
-
-
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
-
# true URI escaping.
-
1
def escape_path(s)
-
escape(s).gsub('+', '%20')
-
end
-
1
module_function :escape_path
-
-
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
-
# target encoding of the string returned, and it defaults to UTF-8
-
1
if defined?(::Encoding)
-
1
def unescape(s, encoding = Encoding::UTF_8)
-
258
URI.decode_www_form_component(s, encoding)
-
end
-
else
-
def unescape(s, encoding = nil)
-
URI.decode_www_form_component(s, encoding)
-
end
-
end
-
1
module_function :unescape
-
-
1
DEFAULT_SEP = /[&;] */n
-
-
1
class << self
-
1
attr_accessor :key_space_limit
-
1
attr_accessor :param_depth_limit
-
1
attr_accessor :multipart_part_limit
-
end
-
-
# The default number of bytes to allow parameter keys to take up.
-
# This helps prevent a rogue client from flooding a Request.
-
1
self.key_space_limit = 65536
-
-
# Default depth at which the parameter parser will raise an exception for
-
# being too deep. This helps prevent SystemStackErrors
-
1
self.param_depth_limit = 100
-
-
# The maximum number of parts a request can contain. Accepting too many part
-
# can lead to the server running out of file handles.
-
# Set to `0` for no limit.
-
# FIXME: RACK_MULTIPART_LIMIT was introduced by mistake and it will be removed in 1.7.0
-
1
self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_LIMIT'] || 128).to_i
-
-
# Stolen from Mongrel, with some small modifications:
-
# Parses a query string by breaking it up at the '&'
-
# and ';' characters. You can also use this to parse
-
# cookies by changing the characters used in the second
-
# parameter (which defaults to '&;').
-
1
def parse_query(qs, d = nil, &unescaper)
-
62
unescaper ||= method(:unescape)
-
-
62
params = KeySpaceConstrainedParams.new
-
-
62
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
-
62
next if p.empty?
-
62
k, v = p.split('=', 2).map(&unescaper)
-
-
62
if cur = params[k]
-
if cur.class == Array
-
params[k] << v
-
else
-
params[k] = [cur, v]
-
end
-
else
-
62
params[k] = v
-
end
-
end
-
-
62
return params.to_params_hash
-
end
-
1
module_function :parse_query
-
-
# parse_nested_query expands a query string into structural types. Supported
-
# types are Arrays, Hashes and basic value types. It is possible to supply
-
# query strings with parameters of conflicting types, in this case a
-
# ParameterTypeError is raised. Users are encouraged to return a 400 in this
-
# case.
-
1
def parse_nested_query(qs, d = nil)
-
86
params = KeySpaceConstrainedParams.new
-
-
86
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
-
201
k, v = p.split('=', 2).map { |s| unescape(s) }
-
-
67
normalize_params(params, k, v)
-
end
-
-
86
return params.to_params_hash
-
rescue ArgumentError => e
-
raise InvalidParameterError, e.message
-
end
-
1
module_function :parse_nested_query
-
-
# normalize_params recursively expands parameters into structural types. If
-
# the structural types represented by two different parameter names are in
-
# conflict, a ParameterTypeError is raised.
-
1
def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit)
-
67
raise RangeError if depth <= 0
-
-
67
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
-
67
k = $1 || ''
-
67
after = $' || ''
-
-
67
return if k.empty?
-
-
67
if after == ""
-
67
params[k] = v
-
elsif after == "["
-
params[name] = v
-
elsif after == "[]"
-
params[k] ||= []
-
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
-
params[k] << v
-
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
-
child_key = $1
-
params[k] ||= []
-
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
-
if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
-
normalize_params(params[k].last, child_key, v, depth - 1)
-
else
-
params[k] << normalize_params(params.class.new, child_key, v, depth - 1)
-
end
-
else
-
params[k] ||= params.class.new
-
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
-
params[k] = normalize_params(params[k], after, v, depth - 1)
-
end
-
-
67
return params
-
end
-
1
module_function :normalize_params
-
-
1
def params_hash_type?(obj)
-
obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
-
end
-
1
module_function :params_hash_type?
-
-
1
def build_query(params)
-
params.map { |k, v|
-
if v.class == Array
-
build_query(v.map { |x| [k, x] })
-
else
-
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
-
end
-
}.join("&")
-
end
-
1
module_function :build_query
-
-
1
def build_nested_query(value, prefix = nil)
-
case value
-
when Array
-
value.map { |v|
-
build_nested_query(v, "#{prefix}[]")
-
}.join("&")
-
when Hash
-
value.map { |k, v|
-
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-
}.reject(&:empty?).join('&')
-
when nil
-
prefix
-
else
-
raise ArgumentError, "value must be a Hash" if prefix.nil?
-
"#{prefix}=#{escape(value)}"
-
end
-
end
-
1
module_function :build_nested_query
-
-
1
def q_values(q_value_header)
-
q_value_header.to_s.split(/\s*,\s*/).map do |part|
-
value, parameters = part.split(/\s*;\s*/, 2)
-
quality = 1.0
-
if md = /\Aq=([\d.]+)/.match(parameters)
-
quality = md[1].to_f
-
end
-
[value, quality]
-
end
-
end
-
1
module_function :q_values
-
-
1
def best_q_match(q_value_header, available_mimes)
-
values = q_values(q_value_header)
-
-
matches = values.map do |req_mime, quality|
-
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
-
next unless match
-
[match, quality]
-
end.compact.sort_by do |match, quality|
-
(match.split('/', 2).count('*') * -10) + quality
-
end.last
-
matches && matches.first
-
end
-
1
module_function :best_q_match
-
-
1
ESCAPE_HTML = {
-
"&" => "&",
-
"<" => "<",
-
">" => ">",
-
"'" => "'",
-
'"' => """,
-
"/" => "/"
-
}
-
1
if //.respond_to?(:encoding)
-
1
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
-
else
-
# On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise
-
# TODO doesn't apply to jruby, so a better condition above might be preferable?
-
ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
-
end
-
-
# Escape ampersands, brackets and quotes to their HTML/XML entities.
-
1
def escape_html(string)
-
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
-
end
-
1
module_function :escape_html
-
-
1
def select_best_encoding(available_encodings, accept_encoding)
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
-
-
expanded_accept_encoding =
-
accept_encoding.map { |m, q|
-
if m == "*"
-
(available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
-
else
-
[[m, q]]
-
end
-
}.inject([]) { |mem, list|
-
mem + list
-
}
-
-
encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
-
-
unless encoding_candidates.include?("identity")
-
encoding_candidates.push("identity")
-
end
-
-
expanded_accept_encoding.each { |m, q|
-
encoding_candidates.delete(m) if q == 0.0
-
}
-
-
return (encoding_candidates & available_encodings)[0]
-
end
-
1
module_function :select_best_encoding
-
-
1
def set_cookie_header!(header, key, value)
-
21
case value
-
when Hash
-
21
domain = "; domain=" + value[:domain] if value[:domain]
-
21
path = "; path=" + value[:path] if value[:path]
-
21
max_age = "; max-age=" + value[:max_age].to_s if value[:max_age]
-
# There is an RFC mess in the area of date formatting for Cookies. Not
-
# only are there contradicting RFCs and examples within RFC text, but
-
# there are also numerous conflicting names of fields and partially
-
# cross-applicable specifications.
-
#
-
# These are best described in RFC 2616 3.3.1. This RFC text also
-
# specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
-
# fixed length format with space-date delimeted fields.
-
#
-
# See also RFC 1123 section 5.2.14.
-
#
-
# RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
-
# in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
-
# the space delimited format. These formats are compliant with RFC 2822.
-
#
-
# For reference, all involved RFCs are:
-
# RFC 822
-
# RFC 1123
-
# RFC 2109
-
# RFC 2616
-
# RFC 2822
-
# RFC 2965
-
# RFC 6265
-
expires = "; expires=" +
-
21
rfc2822(value[:expires].clone.gmtime) if value[:expires]
-
21
secure = "; secure" if value[:secure]
-
21
httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
-
21
same_site =
-
case value[:same_site]
-
when false, nil
-
21
nil
-
when :lax, 'Lax', :Lax
-
'; SameSite=Lax'.freeze
-
when true, :strict, 'Strict', :Strict
-
'; SameSite=Strict'.freeze
-
else
-
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
-
end
-
21
value = value[:value]
-
end
-
21
value = [value] unless Array === value
-
21
cookie = escape(key) + "=" +
-
21
value.map { |v| escape v }.join("&") +
-
"#{domain}#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
-
-
21
case header["Set-Cookie"]
-
when nil, ''
-
21
header["Set-Cookie"] = cookie
-
when String
-
header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
-
when Array
-
header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
-
end
-
-
nil
-
end
-
1
module_function :set_cookie_header!
-
-
1
def delete_cookie_header!(header, key, value = {})
-
case header["Set-Cookie"]
-
when nil, ''
-
cookies = []
-
when String
-
cookies = header["Set-Cookie"].split("\n")
-
when Array
-
cookies = header["Set-Cookie"]
-
end
-
-
cookies.reject! { |cookie|
-
if value[:domain]
-
cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
-
elsif value[:path]
-
cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
-
else
-
cookie =~ /\A#{escape(key)}=/
-
end
-
}
-
-
header["Set-Cookie"] = cookies.join("\n")
-
-
set_cookie_header!(header, key,
-
{:value => '', :path => nil, :domain => nil,
-
:max_age => '0',
-
:expires => Time.at(0) }.merge(value))
-
-
nil
-
end
-
1
module_function :delete_cookie_header!
-
-
# Return the bytesize of String; uses String#size under Ruby 1.8 and
-
# String#bytesize under 1.9.
-
1
if ''.respond_to?(:bytesize)
-
1
def bytesize(string)
-
181
string.bytesize
-
end
-
else
-
def bytesize(string)
-
string.size
-
end
-
end
-
1
module_function :bytesize
-
-
1
def rfc2822(time)
-
time.rfc2822
-
end
-
1
module_function :rfc2822
-
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
-
# of '% %b %Y'.
-
# It assumes that the time is in GMT to comply to the RFC 2109.
-
#
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
-
# that I'm certain someone implemented only that option.
-
# Do not use %a and %b from Time.strptime, it would use localized names for
-
# weekday and month.
-
#
-
1
def rfc2109(time)
-
wday = Time::RFC2822_DAY_NAME[time.wday]
-
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
-
end
-
1
module_function :rfc2109
-
-
# Parses the "Range:" header, if present, into an array of Range objects.
-
# Returns nil if the header is missing or syntactically invalid.
-
# Returns an empty array if none of the ranges are satisfiable.
-
1
def byte_ranges(env, size)
-
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
-
http_range = env['HTTP_RANGE']
-
return nil unless http_range && http_range =~ /bytes=([^;]+)/
-
ranges = []
-
$1.split(/,\s*/).each do |range_spec|
-
return nil unless range_spec =~ /(\d*)-(\d*)/
-
r0,r1 = $1, $2
-
if r0.empty?
-
return nil if r1.empty?
-
# suffix-byte-range-spec, represents trailing suffix of file
-
r0 = size - r1.to_i
-
r0 = 0 if r0 < 0
-
r1 = size - 1
-
else
-
r0 = r0.to_i
-
if r1.empty?
-
r1 = size - 1
-
else
-
r1 = r1.to_i
-
return nil if r1 < r0 # backwards range is syntactically invalid
-
r1 = size-1 if r1 >= size
-
end
-
end
-
ranges << (r0..r1) if r0 <= r1
-
end
-
ranges
-
end
-
1
module_function :byte_ranges
-
-
# Constant time string comparison.
-
#
-
# NOTE: the values compared should be of fixed length, such as strings
-
# that have already been processed by HMAC. This should not be used
-
# on variable length plaintext strings because it could leak length info
-
# via timing attacks.
-
1
def secure_compare(a, b)
-
62
return false unless bytesize(a) == bytesize(b)
-
-
62
l = a.unpack("C*")
-
-
62
r, i = 0, -1
-
2542
b.each_byte { |v| r |= v ^ l[i+=1] }
-
62
r == 0
-
end
-
1
module_function :secure_compare
-
-
# Context allows the use of a compatible middleware at different points
-
# in a request handling stack. A compatible middleware must define
-
# #context which should take the arguments env and app. The first of which
-
# would be the request environment. The second of which would be the rack
-
# application that the request would be forwarded to.
-
1
class Context
-
1
attr_reader :for, :app
-
-
1
def initialize(app_f, app_r)
-
raise 'running context does not respond to #context' unless app_f.respond_to? :context
-
@for, @app = app_f, app_r
-
end
-
-
1
def call(env)
-
@for.context(env, @app)
-
end
-
-
1
def recontext(app)
-
self.class.new(@for, app)
-
end
-
-
1
def context(env, app=@app)
-
recontext(app).call(env)
-
end
-
end
-
-
# A case-insensitive Hash that preserves the original case of a
-
# header when set.
-
1
class HeaderHash < Hash
-
1
def self.new(hash={})
-
71
HeaderHash === hash ? hash : super(hash)
-
end
-
-
1
def initialize(hash={})
-
71
super()
-
71
@names = {}
-
71
hash.each { |k, v| self[k] = v }
-
end
-
-
1
def each
-
213
super do |k, v|
-
560
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
-
end
-
end
-
-
1
def to_hash
-
hash = {}
-
each { |k,v| hash[k] = v }
-
hash
-
end
-
-
1
def [](k)
-
731
super(k) || super(@names[k.downcase])
-
end
-
-
1
def []=(k, v)
-
560
canonical = k.downcase
-
560
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
-
560
@names[k] = @names[canonical] = k
-
560
super k, v
-
end
-
-
1
def delete(k)
-
71
canonical = k.downcase
-
71
result = super @names.delete(canonical)
-
297
@names.delete_if { |name,| name.downcase == canonical }
-
71
result
-
end
-
-
1
def include?(k)
-
@names.include?(k) || @names.include?(k.downcase)
-
end
-
-
1
alias_method :has_key?, :include?
-
1
alias_method :member?, :include?
-
1
alias_method :key?, :include?
-
-
1
def merge!(other)
-
71
other.each { |k, v| self[k] = v }
-
71
self
-
end
-
-
1
def merge(other)
-
71
hash = dup
-
71
hash.merge! other
-
end
-
-
1
def replace(other)
-
clear
-
other.each { |k, v| self[k] = v }
-
self
-
end
-
end
-
-
1
class KeySpaceConstrainedParams
-
1
def initialize(limit = Utils.key_space_limit)
-
148
@limit = limit
-
148
@size = 0
-
148
@params = {}
-
end
-
-
1
def [](key)
-
62
@params[key]
-
end
-
-
1
def []=(key, value)
-
129
@size += key.size if key && !@params.key?(key)
-
129
raise RangeError, 'exceeded available parameter key space' if @size > @limit
-
129
@params[key] = value
-
end
-
-
1
def key?(key)
-
@params.key?(key)
-
end
-
-
1
def to_params_hash
-
148
hash = @params
-
148
hash.keys.each do |key|
-
129
value = hash[key]
-
129
if value.kind_of?(self.class)
-
if value.object_id == self.object_id
-
hash[key] = hash
-
else
-
hash[key] = value.to_params_hash
-
end
-
129
elsif value.kind_of?(Array)
-
value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
-
end
-
end
-
148
hash
-
end
-
end
-
-
# Every standard HTTP code mapped to the appropriate message.
-
# Generated with:
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
-
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
-
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
-
1
HTTP_STATUS_CODES = {
-
100 => 'Continue',
-
101 => 'Switching Protocols',
-
102 => 'Processing',
-
200 => 'OK',
-
201 => 'Created',
-
202 => 'Accepted',
-
203 => 'Non-Authoritative Information',
-
204 => 'No Content',
-
205 => 'Reset Content',
-
206 => 'Partial Content',
-
207 => 'Multi-Status',
-
208 => 'Already Reported',
-
226 => 'IM Used',
-
300 => 'Multiple Choices',
-
301 => 'Moved Permanently',
-
302 => 'Found',
-
303 => 'See Other',
-
304 => 'Not Modified',
-
305 => 'Use Proxy',
-
307 => 'Temporary Redirect',
-
308 => 'Permanent Redirect',
-
400 => 'Bad Request',
-
401 => 'Unauthorized',
-
402 => 'Payment Required',
-
403 => 'Forbidden',
-
404 => 'Not Found',
-
405 => 'Method Not Allowed',
-
406 => 'Not Acceptable',
-
407 => 'Proxy Authentication Required',
-
408 => 'Request Timeout',
-
409 => 'Conflict',
-
410 => 'Gone',
-
411 => 'Length Required',
-
412 => 'Precondition Failed',
-
413 => 'Payload Too Large',
-
414 => 'URI Too Long',
-
415 => 'Unsupported Media Type',
-
416 => 'Range Not Satisfiable',
-
417 => 'Expectation Failed',
-
422 => 'Unprocessable Entity',
-
423 => 'Locked',
-
424 => 'Failed Dependency',
-
426 => 'Upgrade Required',
-
428 => 'Precondition Required',
-
429 => 'Too Many Requests',
-
431 => 'Request Header Fields Too Large',
-
500 => 'Internal Server Error',
-
501 => 'Not Implemented',
-
502 => 'Bad Gateway',
-
503 => 'Service Unavailable',
-
504 => 'Gateway Timeout',
-
505 => 'HTTP Version Not Supported',
-
506 => 'Variant Also Negotiates',
-
507 => 'Insufficient Storage',
-
508 => 'Loop Detected',
-
510 => 'Not Extended',
-
511 => 'Network Authentication Required'
-
}
-
-
# Responses with HTTP status codes that should not have an entity body
-
1
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
-
-
1
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
-
57
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
-
}.flatten]
-
-
1
def status_code(status)
-
if status.is_a?(Symbol)
-
SYMBOL_TO_STATUS_CODE[status] || 500
-
else
-
status.to_i
-
end
-
end
-
1
module_function :status_code
-
-
1
Multipart = Rack::Multipart
-
-
1
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
-
-
1
def clean_path_info(path_info)
-
parts = path_info.split PATH_SEPS
-
-
clean = []
-
-
parts.each do |part|
-
next if part.empty? || part == '.'
-
part == '..' ? clean.pop : clean << part
-
end
-
-
clean.unshift '/' if parts.empty? || parts.first.empty?
-
-
::File.join(*clean)
-
end
-
1
module_function :clean_path_info
-
-
end
-
end
-
1
require 'rack/protection/version'
-
1
require 'rack'
-
-
1
module Rack
-
1
module Protection
-
1
autoload :AuthenticityToken, 'rack/protection/authenticity_token'
-
1
autoload :Base, 'rack/protection/base'
-
1
autoload :EscapedParams, 'rack/protection/escaped_params'
-
1
autoload :FormToken, 'rack/protection/form_token'
-
1
autoload :FrameOptions, 'rack/protection/frame_options'
-
1
autoload :HttpOrigin, 'rack/protection/http_origin'
-
1
autoload :IPSpoofing, 'rack/protection/ip_spoofing'
-
1
autoload :JsonCsrf, 'rack/protection/json_csrf'
-
1
autoload :PathTraversal, 'rack/protection/path_traversal'
-
1
autoload :RemoteReferrer, 'rack/protection/remote_referrer'
-
1
autoload :RemoteToken, 'rack/protection/remote_token'
-
1
autoload :SessionHijacking, 'rack/protection/session_hijacking'
-
1
autoload :XSSHeader, 'rack/protection/xss_header'
-
-
1
def self.new(app, options = {})
-
# does not include: RemoteReferrer, AuthenticityToken and FormToken
-
1
except = Array options[:except]
-
1
use_these = Array options[:use]
-
Rack::Builder.new do
-
1
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
-
1
use ::Rack::Protection::AuthenticityToken,options if use_these.include? :authenticity_token
-
1
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
-
1
use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
-
1
use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
-
1
use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
-
1
use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
-
1
use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
-
1
use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
-
1
use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
-
1
use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
-
1
run app
-
1
end.to_app
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: all
-
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
-
#
-
# Only accepts unsafe HTTP requests if a given access token matches the token
-
# included in the session.
-
#
-
# Compatible with Rails and rack-csrf.
-
#
-
# Options:
-
#
-
# authenticity_param: Defines the param's name that should contain the token on a request.
-
#
-
1
class AuthenticityToken < Base
-
1
default_options :authenticity_param => 'authenticity_token'
-
-
1
def accepts?(env)
-
71
session = session env
-
71
token = session[:csrf] ||= session['_csrf_token'] || random_string
-
safe?(env) ||
-
71
env['HTTP_X_CSRF_TOKEN'] == token ||
-
Request.new(env).params[options[:authenticity_param]] == token
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
1
require 'digest'
-
1
require 'logger'
-
1
require 'uri'
-
-
1
module Rack
-
1
module Protection
-
1
class Base
-
1
DEFAULT_OPTIONS = {
-
:reaction => :default_reaction, :logging => true,
-
:message => 'Forbidden', :encryptor => Digest::SHA1,
-
:session_key => 'rack.session', :status => 403,
-
:allow_empty_referrer => true,
-
:report_key => "protection.failed",
-
:html_types => %w[text/html application/xhtml]
-
}
-
-
1
attr_reader :app, :options
-
-
1
def self.default_options(options)
-
8
define_method(:default_options) { super().merge(options) }
-
end
-
-
1
def self.default_reaction(reaction)
-
4
alias_method(:default_reaction, reaction)
-
end
-
-
1
def default_options
-
8
DEFAULT_OPTIONS
-
end
-
-
1
def initialize(app, options = {})
-
8
@app, @options = app, default_options.merge(options)
-
end
-
-
1
def safe?(env)
-
142
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
-
end
-
-
1
def accepts?(env)
-
raise NotImplementedError, "#{self.class} implementation pending"
-
end
-
-
1
def call(env)
-
284
unless accepts? env
-
instrument env
-
result = react env
-
end
-
284
result or app.call(env)
-
end
-
-
1
def react(env)
-
result = send(options[:reaction], env)
-
result if Array === result and result.size == 3
-
end
-
-
1
def warn(env, message)
-
return unless options[:logging]
-
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
-
l.warn(message)
-
end
-
-
1
def instrument(env)
-
return unless i = options[:instrumenter]
-
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
-
i.instrument('rack.protection', env)
-
end
-
-
1
def deny(env)
-
warn env, "attack prevented by #{self.class}"
-
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
-
end
-
-
1
def report(env)
-
warn env, "attack reported by #{self.class}"
-
env[options[:report_key]] = true
-
end
-
-
1
def session?(env)
-
142
env.include? options[:session_key]
-
end
-
-
1
def session(env)
-
142
return env[options[:session_key]] if session? env
-
fail "you need to set up a session middleware *before* #{self.class}"
-
end
-
-
1
def drop_session(env)
-
session(env).clear if session? env
-
end
-
-
1
def referrer(env)
-
15
ref = env['HTTP_REFERER'].to_s
-
15
return if !options[:allow_empty_referrer] and ref.empty?
-
15
URI.parse(ref).host || Request.new(env).host
-
rescue URI::InvalidURIError
-
end
-
-
1
def origin(env)
-
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
-
end
-
-
1
def random_string(secure = defined? SecureRandom)
-
9
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
-
rescue NotImplementedError
-
random_string false
-
end
-
-
1
def encrypt(value)
-
142
options[:encryptor].hexdigest value.to_s
-
end
-
-
1
alias default_reaction deny
-
-
1
def html?(headers)
-
284
return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
-
142
options[:html_types].include? header.last[/^\w+\/\w+/]
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Clickjacking
-
# Supported browsers:: Internet Explorer 8, Firefox 3.6.9, Opera 10.50,
-
# Safari 4.0, Chrome 4.1.249.1042 and later
-
# More infos:: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header
-
#
-
# Sets X-Frame-Options header to tell the browser avoid embedding the page
-
# in a frame.
-
#
-
# Options:
-
#
-
# frame_options:: Defines who should be allowed to embed the page in a
-
# frame. Use :deny to forbid any embedding, :sameorigin
-
# to allow embedding from the same origin (default).
-
1
class FrameOptions < Base
-
1
default_options :frame_options => :sameorigin
-
-
1
def frame_options
-
@frame_options ||= begin
-
1
frame_options = options[:frame_options]
-
1
frame_options = options[:frame_options].to_s.upcase unless frame_options.respond_to? :to_str
-
1
frame_options.to_str
-
71
end
-
end
-
-
1
def call(env)
-
71
status, headers, body = @app.call(env)
-
71
headers['X-Frame-Options'] ||= frame_options if html? headers
-
71
[status, headers, body]
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: Google Chrome 2, Safari 4 and later
-
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
-
# http://tools.ietf.org/html/draft-abarth-origin
-
#
-
# Does not accept unsafe HTTP requests when value of Origin HTTP request header
-
# does not match default or whitelisted URIs.
-
1
class HttpOrigin < Base
-
1
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
-
1
default_reaction :deny
-
-
1
def base_url(env)
-
15
request = Rack::Request.new(env)
-
15
port = ":#{request.port}" unless request.port == DEFAULT_PORTS[request.scheme]
-
15
"#{request.scheme}://#{request.host}#{port}"
-
end
-
-
1
def accepts?(env)
-
71
return true if safe? env
-
15
return true unless origin = env['HTTP_ORIGIN']
-
15
return true if base_url(env) == origin
-
Array(options[:origin_whitelist]).include? origin
-
end
-
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: IP spoofing
-
# Supported browsers:: all
-
# More infos:: http://blog.c22.cc/2011/04/22/surveymonkey-ip-spoofing/
-
#
-
# Detect (some) IP spoofing attacks.
-
1
class IPSpoofing < Base
-
1
default_reaction :deny
-
-
1
def accepts?(env)
-
71
return true unless env.include? 'HTTP_X_FORWARDED_FOR'
-
ips = env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)
-
return false if env.include? 'HTTP_CLIENT_IP' and not ips.include? env['HTTP_CLIENT_IP']
-
return false if env.include? 'HTTP_X_REAL_IP' and not ips.include? env['HTTP_X_REAL_IP']
-
true
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: all
-
# More infos:: http://flask.pocoo.org/docs/security/#json-security
-
#
-
# JSON GET APIs are vulnerable to being embedded as JavaScript while the
-
# Array prototype has been patched to track data. Checks the referrer
-
# even on GET requests if the content type is JSON.
-
1
class JsonCsrf < Base
-
1
alias react deny
-
-
1
def call(env)
-
71
request = Request.new(env)
-
71
status, headers, body = app.call(env)
-
-
71
if has_vector? request, headers
-
warn env, "attack prevented by #{self.class}"
-
react(env) or [status, headers, body]
-
else
-
71
[status, headers, body]
-
end
-
end
-
-
1
def has_vector?(request, headers)
-
71
return false if request.xhr?
-
71
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
-
origin(request.env).nil? and referrer(request.env) != request.host
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Directory traversal
-
# Supported browsers:: all
-
# More infos:: http://en.wikipedia.org/wiki/Directory_traversal
-
#
-
# Unescapes '/' and '.', expands +path_info+.
-
# Thus <tt>GET /foo/%2e%2e%2fbar</tt> becomes <tt>GET /bar</tt>.
-
1
class PathTraversal < Base
-
1
def call(env)
-
71
path_was = env["PATH_INFO"]
-
71
env["PATH_INFO"] = cleanup path_was if path_was && !path_was.empty?
-
71
app.call env
-
ensure
-
71
env["PATH_INFO"] = path_was
-
end
-
-
1
def cleanup(path)
-
71
if path.respond_to?(:encoding)
-
# Ruby 1.9+ M17N
-
71
encoding = path.encoding
-
71
dot = '.'.encode(encoding)
-
71
slash = '/'.encode(encoding)
-
else
-
# Ruby 1.8
-
dot = '.'
-
slash = '/'
-
end
-
-
71
parts = []
-
71
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash)
-
-
71
unescaped.split(slash).each do |part|
-
158
next if part.empty? or part == dot
-
91
part == '..' ? parts.pop : parts << part
-
end
-
-
71
cleaned = slash + parts.join(slash)
-
71
cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
-
71
cleaned
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: all
-
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
-
#
-
# Only accepts unsafe HTTP requests if a given access token matches the token
-
# included in the session *or* the request comes from the same origin.
-
#
-
# Compatible with Rails and rack-csrf.
-
1
class RemoteToken < AuthenticityToken
-
1
default_reaction :deny
-
-
1
def accepts?(env)
-
71
super or referrer(env) == Request.new(env).host
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Session Hijacking
-
# Supported browsers:: all
-
# More infos:: http://en.wikipedia.org/wiki/Session_hijacking
-
#
-
# Tracks request properties like the user agent in the session and empties
-
# the session if those properties change. This essentially prevents attacks
-
# from Firesheep. Since all headers taken into consideration can be
-
# spoofed, too, this will not prevent determined hijacking attempts.
-
1
class SessionHijacking < Base
-
1
default_reaction :drop_session
-
1
default_options :tracking_key => :tracking, :encrypt_tracking => true,
-
:track => %w[HTTP_USER_AGENT HTTP_ACCEPT_LANGUAGE]
-
-
1
def accepts?(env)
-
71
session = session env
-
71
key = options[:tracking_key]
-
71
if session.include? key
-
186
session[key].all? { |k,v| v == encrypt(env[k]) }
-
else
-
9
session[key] = {}
-
27
options[:track].each { |k| session[key][k] = encrypt(env[k]) }
-
end
-
end
-
-
1
def encrypt(value)
-
142
value = value.to_s.downcase
-
142
options[:encrypt_tracking] ? super(value) : value
-
end
-
end
-
end
-
end
-
1
module Rack
-
1
module Protection
-
1
def self.version
-
VERSION
-
end
-
-
1
SIGNATURE = [1, 5, 3]
-
1
VERSION = SIGNATURE.join('.')
-
-
1
VERSION.extend Comparable
-
1
def VERSION.<=>(other)
-
other = other.split('.').map { |i| i.to_i } if other.respond_to? :split
-
SIGNATURE <=> Array(other)
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Non-permanent XSS
-
# Supported browsers:: Internet Explorer 8 and later
-
# More infos:: http://blogs.msdn.com/b/ie/archive/2008/07/01/ie8-security-part-iv-the-xss-filter.aspx
-
#
-
# Sets X-XSS-Protection header to tell the browser to block attacks.
-
#
-
# Options:
-
# xss_mode:: How the browser should prevent the attack (default: :block)
-
1
class XSSHeader < Base
-
1
default_options :xss_mode => :block, :nosniff => true
-
-
1
def call(env)
-
71
status, headers, body = @app.call(env)
-
71
headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
-
71
headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
-
71
[status, headers, body]
-
end
-
end
-
end
-
end
-
1
module Rack
-
-
1
class MockSession # :nodoc:
-
1
attr_writer :cookie_jar
-
1
attr_reader :default_host
-
-
1
def initialize(app, default_host = Rack::Test::DEFAULT_HOST)
-
@app = app
-
@after_request = []
-
@default_host = default_host
-
@last_request = nil
-
@last_response = nil
-
end
-
-
1
def after_request(&block)
-
@after_request << block
-
end
-
-
1
def clear_cookies
-
@cookie_jar = Rack::Test::CookieJar.new([], @default_host)
-
end
-
-
1
def set_cookie(cookie, uri = nil)
-
cookie_jar.merge(cookie, uri)
-
end
-
-
1
def request(uri, env)
-
env["HTTP_COOKIE"] ||= cookie_jar.for(uri)
-
@last_request = Rack::Request.new(env)
-
status, headers, body = @app.call(@last_request.env)
-
-
@last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush)
-
body.close if body.respond_to?(:close)
-
-
cookie_jar.merge(last_response.headers["Set-Cookie"], uri)
-
-
@after_request.each { |hook| hook.call }
-
-
if @last_response.respond_to?(:finish)
-
@last_response.finish
-
else
-
@last_response
-
end
-
end
-
-
# Return the last request issued in the session. Raises an error if no
-
# requests have been sent yet.
-
1
def last_request
-
raise Rack::Test::Error.new("No request yet. Request a page first.") unless @last_request
-
@last_request
-
end
-
-
# Return the last response received in the session. Raises an error if
-
# no requests have been sent yet.
-
1
def last_response
-
raise Rack::Test::Error.new("No response yet. Request a page first.") unless @last_response
-
@last_response
-
end
-
-
1
def cookie_jar
-
@cookie_jar ||= Rack::Test::CookieJar.new([], @default_host)
-
end
-
-
end
-
-
end
-
1
require "uri"
-
1
require "rack"
-
1
require "rack/mock_session"
-
1
require "rack/test/cookie_jar"
-
1
require "rack/test/mock_digest_request"
-
1
require "rack/test/utils"
-
1
require "rack/test/methods"
-
1
require "rack/test/uploaded_file"
-
-
1
module Rack
-
1
module Test
-
1
VERSION = "0.6.3"
-
-
1
DEFAULT_HOST = "example.org"
-
1
MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1"
-
-
# The common base class for exceptions raised by Rack::Test
-
1
class Error < StandardError; end
-
-
# This class represents a series of requests issued to a Rack app, sharing
-
# a single cookie jar
-
#
-
# Rack::Test::Session's methods are most often called through Rack::Test::Methods,
-
# which will automatically build a session when it's first used.
-
1
class Session
-
1
extend Forwardable
-
1
include Rack::Test::Utils
-
-
1
def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request
-
-
# Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
-
#
-
# Note: Generally, you won't need to initialize a Rack::Test::Session directly.
-
# Instead, you should include Rack::Test::Methods into your testing context.
-
# (See README.rdoc for an example)
-
1
def initialize(mock_session)
-
@headers = {}
-
@env = {}
-
-
if mock_session.is_a?(MockSession)
-
@rack_mock_session = mock_session
-
else
-
@rack_mock_session = MockSession.new(mock_session)
-
end
-
-
@default_host = @rack_mock_session.default_host
-
end
-
-
# Issue a GET request for the given URI with the given params and Rack
-
# environment. Stores the issues request object in #last_request and
-
# the app's response in #last_response. Yield #last_response to a block
-
# if given.
-
#
-
# Example:
-
# get "/"
-
1
def get(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "GET", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a POST request for the given URI. See #get
-
#
-
# Example:
-
# post "/signup", "name" => "Bryan"
-
1
def post(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "POST", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a PUT request for the given URI. See #get
-
#
-
# Example:
-
# put "/"
-
1
def put(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "PUT", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a PATCH request for the given URI. See #get
-
#
-
# Example:
-
# patch "/"
-
1
def patch(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "PATCH", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a DELETE request for the given URI. See #get
-
#
-
# Example:
-
# delete "/"
-
1
def delete(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "DELETE", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue an OPTIONS request for the given URI. See #get
-
#
-
# Example:
-
# options "/"
-
1
def options(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a HEAD request for the given URI. See #get
-
#
-
# Example:
-
# head "/"
-
1
def head(uri, params = {}, env = {}, &block)
-
env = env_for(uri, env.merge(:method => "HEAD", :params => params))
-
process_request(uri, env, &block)
-
end
-
-
# Issue a request to the Rack app for the given URI and optional Rack
-
# environment. Stores the issues request object in #last_request and
-
# the app's response in #last_response. Yield #last_response to a block
-
# if given.
-
#
-
# Example:
-
# request "/"
-
1
def request(uri, env = {}, &block)
-
env = env_for(uri, env)
-
process_request(uri, env, &block)
-
end
-
-
# Set a header to be included on all subsequent requests through the
-
# session. Use a value of nil to remove a previously configured header.
-
#
-
# In accordance with the Rack spec, headers will be included in the Rack
-
# environment hash in HTTP_USER_AGENT form.
-
#
-
# Example:
-
# header "User-Agent", "Firefox"
-
1
def header(name, value)
-
if value.nil?
-
@headers.delete(name)
-
else
-
@headers[name] = value
-
end
-
end
-
-
# Set an env var to be included on all subsequent requests through the
-
# session. Use a value of nil to remove a previously configured env.
-
#
-
# Example:
-
# env "rack.session", {:csrf => 'token'}
-
1
def env(name, value)
-
if value.nil?
-
@env.delete(name)
-
else
-
@env[name] = value
-
end
-
end
-
-
# Set the username and password for HTTP Basic authorization, to be
-
# included in subsequent requests in the HTTP_AUTHORIZATION header.
-
#
-
# Example:
-
# basic_authorize "bryan", "secret"
-
1
def basic_authorize(username, password)
-
encoded_login = ["#{username}:#{password}"].pack("m*")
-
header('Authorization', "Basic #{encoded_login}")
-
end
-
-
1
alias_method :authorize, :basic_authorize
-
-
# Set the username and password for HTTP Digest authorization, to be
-
# included in subsequent requests in the HTTP_AUTHORIZATION header.
-
#
-
# Example:
-
# digest_authorize "bryan", "secret"
-
1
def digest_authorize(username, password)
-
@digest_username = username
-
@digest_password = password
-
end
-
-
# Rack::Test will not follow any redirects automatically. This method
-
# will follow the redirect returned (including setting the Referer header
-
# on the new request) in the last response. If the last response was not
-
# a redirect, an error will be raised.
-
1
def follow_redirect!
-
unless last_response.redirect?
-
raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
-
end
-
-
get(last_response["Location"], {}, { "HTTP_REFERER" => last_request.url })
-
end
-
-
1
private
-
-
1
def env_for(path, env)
-
uri = URI.parse(path)
-
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
uri.host ||= @default_host
-
-
env = default_env.merge(env)
-
-
env["HTTP_HOST"] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(":")
-
-
env.update("HTTPS" => "on") if URI::HTTPS === uri
-
env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" if env[:xhr]
-
-
# TODO: Remove this after Rack 1.1 has been released.
-
# Stringifying and upcasing methods has be commit upstream
-
env["REQUEST_METHOD"] ||= env[:method] ? env[:method].to_s.upcase : "GET"
-
-
if env["REQUEST_METHOD"] == "GET"
-
# merge :params with the query string
-
if params = env[:params]
-
params = parse_nested_query(params) if params.is_a?(String)
-
params.update(parse_nested_query(uri.query))
-
uri.query = build_nested_query(params)
-
end
-
elsif !env.has_key?(:input)
-
env["CONTENT_TYPE"] ||= "application/x-www-form-urlencoded"
-
-
if env[:params].is_a?(Hash)
-
if data = build_multipart(env[:params])
-
env[:input] = data
-
env["CONTENT_LENGTH"] ||= data.length.to_s
-
env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
-
else
-
env[:input] = params_to_string(env[:params])
-
end
-
else
-
env[:input] = env[:params]
-
end
-
end
-
-
env.delete(:params)
-
-
if env.has_key?(:cookie)
-
set_cookie(env.delete(:cookie), uri)
-
end
-
-
Rack::MockRequest.env_for(uri.to_s, env)
-
end
-
-
1
def process_request(uri, env)
-
uri = URI.parse(uri)
-
uri.host ||= @default_host
-
-
@rack_mock_session.request(uri, env)
-
-
if retry_with_digest_auth?(env)
-
auth_env = env.merge({
-
"HTTP_AUTHORIZATION" => digest_auth_header,
-
"rack-test.digest_auth_retry" => true
-
})
-
auth_env.delete('rack.request')
-
process_request(uri.path, auth_env)
-
else
-
yield last_response if block_given?
-
-
last_response
-
end
-
end
-
-
1
def digest_auth_header
-
challenge = last_response["WWW-Authenticate"].split(" ", 2).last
-
params = Rack::Auth::Digest::Params.parse(challenge)
-
-
params.merge!({
-
"username" => @digest_username,
-
"nc" => "00000001",
-
"cnonce" => "nonsensenonce",
-
"uri" => last_request.fullpath,
-
"method" => last_request.env["REQUEST_METHOD"],
-
})
-
-
params["response"] = MockDigestRequest.new(params).response(@digest_password)
-
-
"Digest #{params}"
-
end
-
-
1
def retry_with_digest_auth?(env)
-
last_response.status == 401 &&
-
digest_auth_configured? &&
-
!env["rack-test.digest_auth_retry"]
-
end
-
-
1
def digest_auth_configured?
-
@digest_username
-
end
-
-
1
def default_env
-
{ "rack.test" => true, "REMOTE_ADDR" => "127.0.0.1" }.merge(@env).merge(headers_for_env)
-
end
-
-
1
def headers_for_env
-
converted_headers = {}
-
-
@headers.each do |name, value|
-
env_key = name.upcase.gsub("-", "_")
-
env_key = "HTTP_" + env_key unless "CONTENT_TYPE" == env_key
-
converted_headers[env_key] = value
-
end
-
-
converted_headers
-
end
-
-
1
def params_to_string(params)
-
case params
-
when Hash then build_nested_query(params)
-
when nil then ""
-
else params
-
end
-
end
-
-
end
-
-
1
def self.encoding_aware_strings?
-
defined?(Encoding) && "".respond_to?(:encode)
-
end
-
-
end
-
end
-
1
require "uri"
-
1
require "time"
-
-
1
module Rack
-
1
module Test
-
-
1
class Cookie # :nodoc:
-
1
include Rack::Utils
-
-
# :api: private
-
1
attr_reader :name, :value
-
-
# :api: private
-
1
def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
-
@default_host = default_host
-
uri ||= default_uri
-
-
# separate the name / value pair from the cookie options
-
@name_value_raw, options = raw.split(/[;,] */n, 2)
-
-
@name, @value = parse_query(@name_value_raw, ';').to_a.first
-
@options = parse_query(options, ';')
-
-
@options["domain"] ||= (uri.host || default_host)
-
@options["path"] ||= uri.path.sub(/\/[^\/]*\Z/, "")
-
end
-
-
1
def replaces?(other)
-
[name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
-
end
-
-
# :api: private
-
1
def raw
-
@name_value_raw
-
end
-
-
# :api: private
-
1
def empty?
-
@value.nil? || @value.empty?
-
end
-
-
# :api: private
-
1
def domain
-
@options["domain"]
-
end
-
-
1
def secure?
-
@options.has_key?("secure")
-
end
-
-
# :api: private
-
1
def path
-
@options["path"].strip || "/"
-
end
-
-
# :api: private
-
1
def expires
-
Time.parse(@options["expires"]) if @options["expires"]
-
end
-
-
# :api: private
-
1
def expired?
-
expires && expires < Time.now
-
end
-
-
# :api: private
-
1
def valid?(uri)
-
uri ||= default_uri
-
-
if uri.host.nil?
-
uri.host = @default_host
-
end
-
-
real_domain = domain =~ /^\./ ? domain[1..-1] : domain
-
(!secure? || (secure? && uri.scheme == "https")) &&
-
uri.host =~ Regexp.new("#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE) &&
-
uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
-
end
-
-
# :api: private
-
1
def matches?(uri)
-
! expired? && valid?(uri)
-
end
-
-
# :api: private
-
1
def <=>(other)
-
# Orders the cookies from least specific to most
-
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
-
end
-
-
1
protected
-
-
1
def default_uri
-
URI.parse("//" + @default_host + "/")
-
end
-
-
end
-
-
1
class CookieJar # :nodoc:
-
-
# :api: private
-
1
def initialize(cookies = [], default_host = DEFAULT_HOST)
-
@default_host = default_host
-
@cookies = cookies
-
@cookies.sort!
-
end
-
-
1
def [](name)
-
cookies = hash_for(nil)
-
# TODO: Should be case insensitive
-
cookies[name] && cookies[name].value
-
end
-
-
1
def []=(name, value)
-
merge("#{name}=#{Rack::Utils.escape(value)}")
-
end
-
-
1
def delete(name)
-
@cookies.reject! do |cookie|
-
cookie.name == name
-
end
-
end
-
-
1
def merge(raw_cookies, uri = nil)
-
return unless raw_cookies
-
-
if raw_cookies.is_a? String
-
raw_cookies = raw_cookies.split("\n")
-
raw_cookies.reject!{|c| c.empty? }
-
end
-
-
raw_cookies.each do |raw_cookie|
-
cookie = Cookie.new(raw_cookie, uri, @default_host)
-
self << cookie if cookie.valid?(uri)
-
end
-
end
-
-
1
def <<(new_cookie)
-
@cookies.reject! do |existing_cookie|
-
new_cookie.replaces?(existing_cookie)
-
end
-
-
@cookies << new_cookie
-
@cookies.sort!
-
end
-
-
# :api: private
-
1
def for(uri)
-
hash_for(uri).values.map { |c| c.raw }.join(';')
-
end
-
-
1
def to_hash
-
cookies = {}
-
-
hash_for(nil).each do |name, cookie|
-
cookies[name] = cookie.value
-
end
-
-
return cookies
-
end
-
-
1
protected
-
-
1
def hash_for(uri = nil)
-
cookies = {}
-
-
# The cookies are sorted by most specific first. So, we loop through
-
# all the cookies in order and add it to a hash by cookie name if
-
# the cookie can be sent to the current URI. It's added to the hash
-
# so that when we are done, the cookies will be unique by name and
-
# we'll have grabbed the most specific to the URI.
-
@cookies.each do |cookie|
-
cookies[cookie.name] = cookie if !uri || cookie.matches?(uri)
-
end
-
-
return cookies
-
end
-
-
end
-
-
end
-
end
-
1
require "forwardable"
-
-
1
module Rack
-
1
module Test
-
-
# This module serves as the primary integration point for using Rack::Test
-
# in a testing environment. It depends on an app method being defined in the
-
# same context, and provides the Rack::Test API methods (see Rack::Test::Session
-
# for their documentation).
-
#
-
# Example:
-
#
-
# class HomepageTest < Test::Unit::TestCase
-
# include Rack::Test::Methods
-
#
-
# def app
-
# MyApp.new
-
# end
-
# end
-
1
module Methods
-
1
extend Forwardable
-
-
1
def rack_mock_session(name = :default) # :nodoc:
-
return build_rack_mock_session unless name
-
-
@_rack_mock_sessions ||= {}
-
@_rack_mock_sessions[name] ||= build_rack_mock_session
-
end
-
-
1
def build_rack_mock_session # :nodoc:
-
Rack::MockSession.new(app)
-
end
-
-
1
def rack_test_session(name = :default) # :nodoc:
-
return build_rack_test_session(name) unless name
-
-
@_rack_test_sessions ||= {}
-
@_rack_test_sessions[name] ||= build_rack_test_session(name)
-
end
-
-
1
def build_rack_test_session(name) # :nodoc:
-
Rack::Test::Session.new(rack_mock_session(name))
-
end
-
-
1
def current_session # :nodoc:
-
rack_test_session(_current_session_names.last)
-
end
-
-
1
def with_session(name) # :nodoc:
-
_current_session_names.push(name)
-
yield rack_test_session(name)
-
_current_session_names.pop
-
end
-
-
1
def _current_session_names # :nodoc:
-
@_current_session_names ||= [:default]
-
end
-
-
1
METHODS = [
-
:request,
-
:get,
-
:post,
-
:put,
-
:patch,
-
:delete,
-
:options,
-
:head,
-
:follow_redirect!,
-
:header,
-
:env,
-
:set_cookie,
-
:clear_cookies,
-
:authorize,
-
:basic_authorize,
-
:digest_authorize,
-
:last_response,
-
:last_request
-
]
-
-
1
def_delegators :current_session, *METHODS
-
end
-
end
-
end
-
1
module Rack
-
1
module Test
-
-
1
class MockDigestRequest # :nodoc:
-
-
1
def initialize(params)
-
@params = params
-
end
-
-
1
def method_missing(sym)
-
if @params.has_key? k = sym.to_s
-
return @params[k]
-
end
-
-
super
-
end
-
-
1
def method
-
@params['method']
-
end
-
-
1
def response(password)
-
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
-
end
-
-
end
-
-
end
-
end
-
1
require "tempfile"
-
1
require "fileutils"
-
-
1
module Rack
-
1
module Test
-
-
# Wraps a Tempfile with a content type. Including one or more UploadedFile's
-
# in the params causes Rack::Test to build and issue a multipart request.
-
#
-
# Example:
-
# post "/photos", "file" => Rack::Test::UploadedFile.new("me.jpg", "image/jpeg")
-
1
class UploadedFile
-
-
# The filename, *not* including the path, of the "uploaded" file
-
1
attr_reader :original_filename
-
-
# The tempfile
-
1
attr_reader :tempfile
-
-
# The content type of the "uploaded" file
-
1
attr_accessor :content_type
-
-
1
def initialize(path, content_type = "text/plain", binary = false)
-
raise "#{path} file does not exist" unless ::File.exist?(path)
-
-
@content_type = content_type
-
@original_filename = ::File.basename(path)
-
-
@tempfile = Tempfile.new(@original_filename)
-
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
-
@tempfile.binmode if binary
-
-
FileUtils.copy_file(path, @tempfile.path)
-
end
-
-
1
def path
-
@tempfile.path
-
end
-
-
1
alias_method :local_path, :path
-
-
1
def method_missing(method_name, *args, &block) #:nodoc:
-
@tempfile.__send__(method_name, *args, &block)
-
end
-
-
1
def respond_to?(method_name, include_private = false) #:nodoc:
-
@tempfile.respond_to?(method_name, include_private) || super
-
end
-
-
end
-
-
end
-
end
-
1
module Rack
-
1
module Test
-
-
1
module Utils # :nodoc:
-
1
include Rack::Utils
-
-
1
def build_nested_query(value, prefix = nil)
-
case value
-
when Array
-
value.map do |v|
-
unless unescape(prefix) =~ /\[\]$/
-
prefix = "#{prefix}[]"
-
end
-
build_nested_query(v, "#{prefix}")
-
end.join("&")
-
when Hash
-
value.map do |k, v|
-
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-
end.join("&")
-
when NilClass
-
prefix.to_s
-
else
-
"#{prefix}=#{escape(value)}"
-
end
-
end
-
-
1
module_function :build_nested_query
-
-
1
def build_multipart(params, first = true)
-
if first
-
unless params.is_a?(Hash)
-
raise ArgumentError, "value must be a Hash"
-
end
-
-
multipart = false
-
query = lambda { |value|
-
case value
-
when Array
-
value.each(&query)
-
when Hash
-
value.values.each(&query)
-
when UploadedFile
-
multipart = true
-
end
-
}
-
params.values.each(&query)
-
return nil unless multipart
-
end
-
-
flattened_params = Hash.new
-
-
params.each do |key, value|
-
k = first ? key.to_s : "[#{key}]"
-
-
case value
-
when Array
-
value.map do |v|
-
-
if (v.is_a?(Hash))
-
nested_params = {}
-
build_multipart(v, false).each { |subkey, subvalue|
-
nested_params[subkey] = subvalue
-
}
-
flattened_params["#{k}[]"] ||= []
-
flattened_params["#{k}[]"] << nested_params
-
else
-
flattened_params["#{k}[]"] = value
-
end
-
-
end
-
when Hash
-
build_multipart(value, false).each { |subkey, subvalue|
-
flattened_params[k + subkey] = subvalue
-
}
-
else
-
flattened_params[k] = value
-
end
-
end
-
-
if first
-
build_parts(flattened_params)
-
else
-
flattened_params
-
end
-
end
-
-
1
module_function :build_multipart
-
-
1
private
-
1
def build_parts(parameters)
-
get_parts(parameters).join + "--#{MULTIPART_BOUNDARY}--\r"
-
end
-
-
1
def get_parts(parameters)
-
parameters.map { |name, value|
-
if name =~ /\[\]\Z/ && value.is_a?(Array) && value.all? {|v| v.is_a?(Hash)}
-
value.map { |hash|
-
new_value = {}
-
hash.each { |k, v| new_value[name+k] = v }
-
get_parts(new_value).join
-
}.join
-
else
-
if value.respond_to?(:original_filename)
-
build_file_part(name, value)
-
-
elsif value.is_a?(Array) and value.all? { |v| v.respond_to?(:original_filename) }
-
value.map do |v|
-
build_file_part(name, v)
-
end.join
-
-
else
-
primitive_part = build_primitive_part(name, value)
-
Rack::Test.encoding_aware_strings? ? primitive_part.force_encoding('BINARY') : primitive_part
-
end
-
end
-
}
-
end
-
-
1
def build_primitive_part(parameter_name, value)
-
unless value.is_a? Array
-
value = [value]
-
end
-
value.map do |v|
-
<<-EOF
-
--#{MULTIPART_BOUNDARY}\r
-
Content-Disposition: form-data; name="#{parameter_name}"\r
-
\r
-
#{v}\r
-
EOF
-
end.join
-
end
-
-
1
def build_file_part(parameter_name, uploaded_file)
-
::File.open(uploaded_file.path, "rb") do |physical_file|
-
physical_file.set_encoding(Encoding::BINARY) if physical_file.respond_to?(:set_encoding)
-
<<-EOF
-
--#{MULTIPART_BOUNDARY}\r
-
Content-Disposition: form-data; name="#{parameter_name}"; filename="#{escape(uploaded_file.original_filename)}"\r
-
Content-Type: #{uploaded_file.content_type}\r
-
Content-Length: #{::File.stat(uploaded_file.path).size}\r
-
\r
-
#{physical_file.read}\r
-
EOF
-
end
-
end
-
-
end
-
-
end
-
end
-
1
require 'rspec/core'
-
1
require 'rspec/version'
-
-
1
module RSpec # :nodoc:
-
1
module Version # :nodoc:
-
1
STRING = '3.5.0'
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/helpers"
-
1
require 'stringio'
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# RSpec's built-in formatters are all subclasses of
-
# RSpec::Core::Formatters::BaseTextFormatter.
-
#
-
# @see RSpec::Core::Formatters::BaseTextFormatter
-
# @see RSpec::Core::Reporter
-
# @see RSpec::Core::Formatters::Protocol
-
1
class BaseFormatter
-
# All formatters inheriting from this formatter will receive these
-
# notifications.
-
1
Formatters.register self, :start, :example_group_started, :close
-
1
attr_accessor :example_group
-
1
attr_reader :output
-
-
# @api public
-
# @param output [IO] the formatter output
-
# @see RSpec::Core::Formatters::Protocol#initialize
-
1
def initialize(output)
-
1
@output = output || StringIO.new
-
1
@example_group = nil
-
end
-
-
# @api public
-
#
-
# @param notification [StartNotification]
-
# @see RSpec::Core::Formatters::Protocol#start
-
1
def start(notification)
-
1
start_sync_output
-
1
@example_count = notification.count
-
end
-
-
# @api public
-
#
-
# @param notification [GroupNotification] containing example_group
-
# subclass of `RSpec::Core::ExampleGroup`
-
# @see RSpec::Core::Formatters::Protocol#example_group_started
-
1
def example_group_started(notification)
-
6
@example_group = notification.group
-
end
-
-
# @api public
-
#
-
# @param _notification [NullNotification] (Ignored)
-
# @see RSpec::Core::Formatters::Protocol#close
-
1
def close(_notification)
-
restore_sync_output
-
end
-
-
1
private
-
-
1
def start_sync_output
-
1
@old_sync, output.sync = output.sync, true if output_supports_sync
-
end
-
-
1
def restore_sync_output
-
output.sync = @old_sync if output_supports_sync && !output.closed?
-
end
-
-
1
def output_supports_sync
-
1
output.respond_to?(:sync=)
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/base_formatter"
-
1
RSpec::Support.require_rspec_core "formatters/console_codes"
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# Base for all of RSpec's built-in formatters. See
-
# RSpec::Core::Formatters::BaseFormatter to learn more about all of the
-
# methods called by the reporter.
-
#
-
# @see RSpec::Core::Formatters::BaseFormatter
-
# @see RSpec::Core::Reporter
-
1
class BaseTextFormatter < BaseFormatter
-
1
Formatters.register self,
-
:message, :dump_summary, :dump_failures, :dump_pending, :seed
-
-
# @api public
-
#
-
# Used by the reporter to send messages to the output stream.
-
#
-
# @param notification [MessageNotification] containing message
-
1
def message(notification)
-
output.puts notification.message
-
end
-
-
# @api public
-
#
-
# Dumps detailed information about each example failure.
-
#
-
# @param notification [NullNotification]
-
1
def dump_failures(notification)
-
1
return if notification.failure_notifications.empty?
-
output.puts notification.fully_formatted_failed_examples
-
end
-
-
# @api public
-
#
-
# This method is invoked after the dumping of examples and failures.
-
# Each parameter is assigned to a corresponding attribute.
-
#
-
# @param summary [SummaryNotification] containing duration,
-
# example_count, failure_count and pending_count
-
1
def dump_summary(summary)
-
1
output.puts summary.fully_formatted
-
end
-
-
# @private
-
1
def dump_pending(notification)
-
1
return if notification.pending_examples.empty?
-
1
output.puts notification.fully_formatted_pending_examples
-
end
-
-
# @private
-
1
def seed(notification)
-
2
return unless notification.seed_used?
-
output.puts notification.fully_formatted
-
end
-
-
# @api public
-
#
-
# Invoked at the very end, `close` allows the formatter to clean
-
# up resources, e.g. open streams, etc.
-
#
-
# @param _notification [NullNotification] (Ignored)
-
1
def close(_notification)
-
1
return unless IO === output
-
1
return if output.closed?
-
-
1
output.puts
-
-
1
output.flush
-
1
output.close unless output == $stdout
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# ConsoleCodes provides helpers for formatting console output
-
# with ANSI codes, e.g. color's and bold.
-
1
module ConsoleCodes
-
# @private
-
1
VT100_CODES =
-
{
-
:black => 30,
-
:red => 31,
-
:green => 32,
-
:yellow => 33,
-
:blue => 34,
-
:magenta => 35,
-
:cyan => 36,
-
:white => 37,
-
:bold => 1,
-
}
-
# @private
-
1
VT100_CODE_VALUES = VT100_CODES.invert
-
-
1
module_function
-
-
# @private
-
1
CONFIG_COLORS_TO_METHODS = Configuration.instance_methods.grep(/_color\z/).inject({}) do |hash, method|
-
6
hash[method.to_s.sub(/_color\z/, '').to_sym] = method
-
6
hash
-
end
-
-
# Fetches the correct code for the supplied symbol, or checks
-
# that a code is valid. Defaults to white (37).
-
#
-
# @param code_or_symbol [Symbol, Fixnum] Symbol or code to check
-
# @return [Fixnum] a console code
-
1
def console_code_for(code_or_symbol)
-
53
if (config_method = CONFIG_COLORS_TO_METHODS[code_or_symbol])
-
26
console_code_for RSpec.configuration.__send__(config_method)
-
27
elsif VT100_CODE_VALUES.key?(code_or_symbol)
-
code_or_symbol
-
else
-
27
VT100_CODES.fetch(code_or_symbol) do
-
console_code_for(:white)
-
end
-
end
-
end
-
-
# Wraps a piece of text in ANSI codes with the supplied code. Will
-
# only apply the control code if `RSpec.configuration.color_enabled?`
-
# returns true.
-
#
-
# @param text [String] the text to wrap
-
# @param code_or_symbol [Symbol, Fixnum] the desired control code
-
# @return [String] the wrapped text
-
1
def wrap(text, code_or_symbol)
-
27
if RSpec.configuration.color_enabled?
-
27
"\e[#{console_code_for(code_or_symbol)}m#{text}\e[0m"
-
else
-
text
-
end
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# @private
-
1
class ProgressFormatter < BaseTextFormatter
-
1
Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump
-
-
1
def example_passed(_notification)
-
14
output.print ConsoleCodes.wrap('.', :success)
-
end
-
-
1
def example_pending(_notification)
-
3
output.print ConsoleCodes.wrap('*', :pending)
-
end
-
-
1
def example_failed(_notification)
-
output.print ConsoleCodes.wrap('F', :failure)
-
end
-
-
1
def start_dump(_notification)
-
1
output.puts
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/mocks'
-
-
1
module RSpec
-
1
module Core
-
1
module MockingAdapters
-
# @private
-
1
module RSpec
-
1
include ::RSpec::Mocks::ExampleMethods
-
-
1
def self.framework_name
-
1
:rspec
-
end
-
-
1
def self.configuration
-
1
::RSpec::Mocks.configuration
-
end
-
-
1
def setup_mocks_for_rspec
-
14
::RSpec::Mocks.setup
-
end
-
-
1
def verify_mocks_for_rspec
-
14
::RSpec::Mocks.verify
-
end
-
-
1
def teardown_mocks_for_rspec
-
14
::RSpec::Mocks.teardown
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/support'
-
1
RSpec::Support.require_rspec_support "caller_filter"
-
1
RSpec::Support.require_rspec_support "warnings"
-
1
RSpec::Support.require_rspec_support "object_formatter"
-
-
1
require 'rspec/matchers'
-
-
7
RSpec::Support.define_optimized_require_for_rspec(:expectations) { |f| require_relative(f) }
-
-
%w[
-
expectation_target
-
configuration
-
fail_with
-
handler
-
version
-
6
].each { |file| RSpec::Support.require_rspec_expectations(file) }
-
-
1
module RSpec
-
# RSpec::Expectations provides a simple, readable API to express
-
# the expected outcomes in a code example. To express an expected
-
# outcome, wrap an object or block in `expect`, call `to` or `to_not`
-
# (aliased as `not_to`) and pass it a matcher object:
-
#
-
# expect(order.total).to eq(Money.new(5.55, :USD))
-
# expect(list).to include(user)
-
# expect(message).not_to match(/foo/)
-
# expect { do_something }.to raise_error
-
#
-
# The last form (the block form) is needed to match against ruby constructs
-
# that are not objects, but can only be observed when executing a block
-
# of code. This includes raising errors, throwing symbols, yielding,
-
# and changing values.
-
#
-
# When `expect(...).to` is invoked with a matcher, it turns around
-
# and calls `matcher.matches?(<object wrapped by expect>)`. For example,
-
# in the expression:
-
#
-
# expect(order.total).to eq(Money.new(5.55, :USD))
-
#
-
# ...`eq(Money.new(5.55, :USD))` returns a matcher object, and it results
-
# in the equivalent of `eq.matches?(order.total)`. If `matches?` returns
-
# `true`, the expectation is met and execution continues. If `false`, then
-
# the spec fails with the message returned by `eq.failure_message`.
-
#
-
# Given the expression:
-
#
-
# expect(order.entries).not_to include(entry)
-
#
-
# ...the `not_to` method (also available as `to_not`) invokes the equivalent of
-
# `include.matches?(order.entries)`, but it interprets `false` as success, and
-
# `true` as a failure, using the message generated by
-
# `include.failure_message_when_negated`.
-
#
-
# rspec-expectations ships with a standard set of useful matchers, and writing
-
# your own matchers is quite simple.
-
#
-
# See [RSpec::Matchers](../RSpec/Matchers) for more information about the
-
# built-in matchers that ship with rspec-expectations, and how to write your
-
# own custom matchers.
-
1
module Expectations
-
# Exception raised when an expectation fails.
-
#
-
# @note We subclass Exception so that in a stub implementation if
-
# the user sets an expectation, it can't be caught in their
-
# code by a bare `rescue`.
-
# @api public
-
1
class ExpectationNotMetError < Exception
-
end
-
-
# Exception raised from `aggregate_failures` when multiple expectations fail.
-
#
-
# @note The constant is defined here but the extensive logic of this class
-
# is lazily defined when `FailureAggregator` is autoloaded, since we do
-
# not need to waste time defining that functionality unless
-
# `aggregate_failures` is used.
-
1
class MultipleExpectationsNotMetError < ExpectationNotMetError
-
end
-
-
1
autoload :FailureAggregator, "rspec/expectations/failure_aggregator"
-
end
-
end
-
1
RSpec::Support.require_rspec_expectations "syntax"
-
-
1
module RSpec
-
1
module Expectations
-
# Provides configuration options for rspec-expectations.
-
# If you are using rspec-core, you can access this via a
-
# block passed to `RSpec::Core::Configuration#expect_with`.
-
# Otherwise, you can access it via RSpec::Expectations.configuration.
-
#
-
# @example
-
# RSpec.configure do |rspec|
-
# rspec.expect_with :rspec do |c|
-
# # c is the config object
-
# end
-
# end
-
#
-
# # or
-
#
-
# RSpec::Expectations.configuration
-
1
class Configuration
-
# @private
-
1
FALSE_POSITIVE_BEHAVIOURS =
-
{
-
:warn => lambda { |message| RSpec.warning message },
-
:raise => lambda { |message| raise ArgumentError, message },
-
:nothing => lambda { |_| true },
-
}
-
-
1
def initialize
-
1
@on_potential_false_positives = :warn
-
end
-
-
# Configures the supported syntax.
-
# @param [Array<Symbol>, Symbol] values the syntaxes to enable
-
# @example
-
# RSpec.configure do |rspec|
-
# rspec.expect_with :rspec do |c|
-
# c.syntax = :should
-
# # or
-
# c.syntax = :expect
-
# # or
-
# c.syntax = [:should, :expect]
-
# end
-
# end
-
1
def syntax=(values)
-
1
if Array(values).include?(:expect)
-
1
Expectations::Syntax.enable_expect
-
else
-
Expectations::Syntax.disable_expect
-
end
-
-
1
if Array(values).include?(:should)
-
1
Expectations::Syntax.enable_should
-
else
-
Expectations::Syntax.disable_should
-
end
-
end
-
-
# The list of configured syntaxes.
-
# @return [Array<Symbol>] the list of configured syntaxes.
-
# @example
-
# unless RSpec::Matchers.configuration.syntax.include?(:expect)
-
# raise "this RSpec extension gem requires the rspec-expectations `:expect` syntax"
-
# end
-
1
def syntax
-
syntaxes = []
-
syntaxes << :should if Expectations::Syntax.should_enabled?
-
syntaxes << :expect if Expectations::Syntax.expect_enabled?
-
syntaxes
-
end
-
-
1
if ::RSpec.respond_to?(:configuration)
-
1
def color?
-
::RSpec.configuration.color_enabled?
-
end
-
else
-
# Indicates whether or not diffs should be colored.
-
# Delegates to rspec-core's color option if rspec-core
-
# is loaded; otherwise you can set it here.
-
attr_writer :color
-
-
# Indicates whether or not diffs should be colored.
-
# Delegates to rspec-core's color option if rspec-core
-
# is loaded; otherwise you can set it here.
-
def color?
-
defined?(@color) && @color
-
end
-
end
-
-
# Adds `should` and `should_not` to the given classes
-
# or modules. This can be used to ensure `should` works
-
# properly on things like proxy objects (particular
-
# `Delegator`-subclassed objects on 1.8).
-
#
-
# @param [Array<Module>] modules the list of classes or modules
-
# to add `should` and `should_not` to.
-
1
def add_should_and_should_not_to(*modules)
-
modules.each do |mod|
-
Expectations::Syntax.enable_should(mod)
-
end
-
end
-
-
# Sets or gets the backtrace formatter. The backtrace formatter should
-
# implement `#format_backtrace(Array<String>)`. This is used
-
# to format backtraces of errors handled by the `raise_error`
-
# matcher.
-
#
-
# If you are using rspec-core, rspec-core's backtrace formatting
-
# will be used (including respecting the presence or absence of
-
# the `--backtrace` option).
-
#
-
# @!attribute [rw] backtrace_formatter
-
1
attr_writer :backtrace_formatter
-
1
def backtrace_formatter
-
@backtrace_formatter ||= if defined?(::RSpec.configuration.backtrace_formatter)
-
::RSpec.configuration.backtrace_formatter
-
else
-
NullBacktraceFormatter
-
end
-
end
-
-
# Sets if custom matcher descriptions and failure messages
-
# should include clauses from methods defined using `chain`.
-
# @param value [Boolean]
-
1
attr_writer :include_chain_clauses_in_custom_matcher_descriptions
-
-
# Indicates whether or not custom matcher descriptions and failure messages
-
# should include clauses from methods defined using `chain`. It is
-
# false by default for backwards compatibility.
-
1
def include_chain_clauses_in_custom_matcher_descriptions?
-
@include_chain_clauses_in_custom_matcher_descriptions ||= false
-
end
-
-
# @private
-
1
def reset_syntaxes_to_default
-
1
self.syntax = [:should, :expect]
-
1
RSpec::Expectations::Syntax.warn_about_should!
-
end
-
-
# @api private
-
# Null implementation of a backtrace formatter used by default
-
# when rspec-core is not loaded. Does no filtering.
-
1
NullBacktraceFormatter = Module.new do
-
1
def self.format_backtrace(backtrace)
-
backtrace
-
end
-
end
-
-
# Configures whether RSpec will warn about matcher use which will
-
# potentially cause false positives in tests.
-
#
-
# @param [Boolean] boolean
-
1
def warn_about_potential_false_positives=(boolean)
-
if boolean
-
self.on_potential_false_positives = :warn
-
elsif warn_about_potential_false_positives?
-
self.on_potential_false_positives = :nothing
-
else
-
# no-op, handler is something else
-
end
-
end
-
#
-
# Configures what RSpec will do about matcher use which will
-
# potentially cause false positives in tests.
-
#
-
# @param [Symbol] behavior can be set to :warn, :raise or :nothing
-
1
def on_potential_false_positives=(behavior)
-
unless FALSE_POSITIVE_BEHAVIOURS.key?(behavior)
-
raise ArgumentError, "Supported values are: #{FALSE_POSITIVE_BEHAVIOURS.keys}"
-
end
-
@on_potential_false_positives = behavior
-
end
-
-
# Indicates what RSpec will do about matcher use which will
-
# potentially cause false positives in tests, generally you want to
-
# avoid such scenarios so this defaults to `true`.
-
1
attr_reader :on_potential_false_positives
-
-
# Indicates whether RSpec will warn about matcher use which will
-
# potentially cause false positives in tests, generally you want to
-
# avoid such scenarios so this defaults to `true`.
-
1
def warn_about_potential_false_positives?
-
on_potential_false_positives == :warn
-
end
-
-
# @private
-
1
def false_positives_handler
-
FALSE_POSITIVE_BEHAVIOURS.fetch(@on_potential_false_positives)
-
end
-
end
-
-
# The configuration object.
-
# @return [RSpec::Expectations::Configuration] the configuration object
-
1
def self.configuration
-
2
@configuration ||= Configuration.new
-
end
-
-
# set default syntax
-
1
configuration.reset_syntaxes_to_default
-
end
-
end
-
1
module RSpec
-
1
module Expectations
-
# Wraps the target of an expectation.
-
#
-
# @example
-
# expect(something) # => ExpectationTarget wrapping something
-
# expect { do_something } # => ExpectationTarget wrapping the block
-
#
-
# # used with `to`
-
# expect(actual).to eq(3)
-
#
-
# # with `not_to`
-
# expect(actual).not_to eq(3)
-
#
-
# @note `ExpectationTarget` is not intended to be instantiated
-
# directly by users. Use `expect` instead.
-
1
class ExpectationTarget
-
# @private
-
# Used as a sentinel value to be able to tell when the user
-
# did not pass an argument. We can't use `nil` for that because
-
# `nil` is a valid value to pass.
-
1
UndefinedValue = Module.new
-
-
# @note this name aligns with `Minitest::Expectation` so that our
-
# {InstanceMethods} module can be included in that class when
-
# used in a Minitest context.
-
# @return [Object] the target of the expectation
-
1
attr_reader :target
-
-
# @api private
-
1
def initialize(value)
-
15
@target = value
-
end
-
-
# @private
-
1
def self.for(value, block)
-
15
if UndefinedValue.equal?(value)
-
6
unless block
-
raise ArgumentError, "You must pass either an argument or a block to `expect`."
-
end
-
6
BlockExpectationTarget.new(block)
-
9
elsif block
-
raise ArgumentError, "You cannot pass both an argument and a block to `expect`."
-
else
-
9
new(value)
-
end
-
end
-
-
# Defines instance {ExpectationTarget} instance methods. These are defined
-
# in a module so we can include it in `Minitest::Expectation` when
-
# `rspec/expectations/minitest_integration` is laoded in order to
-
# support usage with Minitest.
-
1
module InstanceMethods
-
# Runs the given expectation, passing if `matcher` returns true.
-
# @example
-
# expect(value).to eq(5)
-
# expect { perform }.to raise_error
-
# @param [Matcher]
-
# matcher
-
# @param [String or Proc] message optional message to display when the expectation fails
-
# @return [Boolean] true if the expectation succeeds (else raises)
-
# @see RSpec::Matchers
-
1
def to(matcher=nil, message=nil, &block)
-
10
prevent_operator_matchers(:to) unless matcher
-
10
RSpec::Expectations::PositiveExpectationHandler.handle_matcher(target, matcher, message, &block)
-
end
-
-
# Runs the given expectation, passing if `matcher` returns false.
-
# @example
-
# expect(value).not_to eq(5)
-
# @param [Matcher]
-
# matcher
-
# @param [String or Proc] message optional message to display when the expectation fails
-
# @return [Boolean] false if the negative expectation succeeds (else raises)
-
# @see RSpec::Matchers
-
1
def not_to(matcher=nil, message=nil, &block)
-
5
prevent_operator_matchers(:not_to) unless matcher
-
5
RSpec::Expectations::NegativeExpectationHandler.handle_matcher(target, matcher, message, &block)
-
end
-
1
alias to_not not_to
-
-
1
private
-
-
1
def prevent_operator_matchers(verb)
-
raise ArgumentError, "The expect syntax does not support operator matchers, " \
-
"so you must pass a matcher to `##{verb}`."
-
end
-
end
-
-
1
include InstanceMethods
-
end
-
-
# @private
-
# Validates the provided matcher to ensure it supports block
-
# expectations, in order to avoid user confusion when they
-
# use a block thinking the expectation will be on the return
-
# value of the block rather than the block itself.
-
1
class BlockExpectationTarget < ExpectationTarget
-
1
def to(matcher, message=nil, &block)
-
2
enforce_block_expectation(matcher)
-
2
super
-
end
-
-
1
def not_to(matcher, message=nil, &block)
-
4
enforce_block_expectation(matcher)
-
4
super
-
end
-
1
alias to_not not_to
-
-
1
private
-
-
1
def enforce_block_expectation(matcher)
-
6
return if supports_block_expectations?(matcher)
-
-
raise ExpectationNotMetError, "You must pass an argument rather than a block to use the provided " \
-
"matcher (#{RSpec::Support::ObjectFormatter.format(matcher)}), or the matcher must implement " \
-
"`supports_block_expectations?`."
-
end
-
-
1
def supports_block_expectations?(matcher)
-
6
matcher.supports_block_expectations?
-
rescue NoMethodError
-
false
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Expectations
-
1
class << self
-
# @private
-
1
def differ
-
RSpec::Support::Differ.new(
-
:object_preparer => lambda { |object| RSpec::Matchers::Composable.surface_descriptions_in(object) },
-
:color => RSpec::Matchers.configuration.color?
-
)
-
end
-
-
# Raises an RSpec::Expectations::ExpectationNotMetError with message.
-
# @param [String] message
-
# @param [Object] expected
-
# @param [Object] actual
-
#
-
# Adds a diff to the failure message when `expected` and `actual` are
-
# both present.
-
1
def fail_with(message, expected=nil, actual=nil)
-
unless message
-
raise ArgumentError, "Failure message is nil. Does your matcher define the " \
-
"appropriate failure_message[_when_negated] method to return a string?"
-
end
-
-
message = ::RSpec::Matchers::ExpectedsForMultipleDiffs.from(expected).message_with_diff(message, differ, actual)
-
-
RSpec::Support.notify_failure(RSpec::Expectations::ExpectationNotMetError.new message)
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Expectations
-
# @private
-
1
module ExpectationHelper
-
1
def self.check_message(msg)
-
15
unless msg.nil? || msg.respond_to?(:to_str) || msg.respond_to?(:call)
-
::Kernel.warn [
-
"WARNING: ignoring the provided expectation message argument (",
-
msg.inspect,
-
") since it is not a string or a proc."
-
].join
-
end
-
end
-
-
# Returns an RSpec-3+ compatible matcher, wrapping a legacy one
-
# in an adapter if necessary.
-
#
-
# @private
-
1
def self.modern_matcher_from(matcher)
-
LegacyMatcherAdapter::RSpec2.wrap(matcher) ||
-
15
LegacyMatcherAdapter::RSpec1.wrap(matcher) || matcher
-
end
-
-
1
def self.with_matcher(handler, matcher, message)
-
15
check_message(message)
-
15
matcher = modern_matcher_from(matcher)
-
15
yield matcher
-
ensure
-
15
::RSpec::Matchers.last_expectation_handler = handler
-
15
::RSpec::Matchers.last_matcher = matcher
-
end
-
-
1
def self.handle_failure(matcher, message, failure_message_method)
-
message = message.call if message.respond_to?(:call)
-
message ||= matcher.__send__(failure_message_method)
-
-
if matcher.respond_to?(:diffable?) && matcher.diffable?
-
::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
-
else
-
::RSpec::Expectations.fail_with message
-
end
-
end
-
end
-
-
# @private
-
1
class PositiveExpectationHandler
-
1
def self.handle_matcher(actual, initial_matcher, message=nil, &block)
-
10
ExpectationHelper.with_matcher(self, initial_matcher, message) do |matcher|
-
10
return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) unless initial_matcher
-
10
matcher.matches?(actual, &block) || ExpectationHelper.handle_failure(matcher, message, :failure_message)
-
end
-
end
-
-
1
def self.verb
-
"should"
-
end
-
-
1
def self.should_method
-
:should
-
end
-
-
1
def self.opposite_should_method
-
:should_not
-
end
-
end
-
-
# @private
-
1
class NegativeExpectationHandler
-
1
def self.handle_matcher(actual, initial_matcher, message=nil, &block)
-
5
ExpectationHelper.with_matcher(self, initial_matcher, message) do |matcher|
-
5
return ::RSpec::Matchers::BuiltIn::NegativeOperatorMatcher.new(actual) unless initial_matcher
-
5
does_not_match?(matcher, actual, &block) || ExpectationHelper.handle_failure(matcher, message, :failure_message_when_negated)
-
end
-
end
-
-
1
def self.does_not_match?(matcher, actual, &block)
-
5
if matcher.respond_to?(:does_not_match?)
-
5
matcher.does_not_match?(actual, &block)
-
else
-
!matcher.matches?(actual, &block)
-
end
-
end
-
-
1
def self.verb
-
"should not"
-
end
-
-
1
def self.should_method
-
:should_not
-
end
-
-
1
def self.opposite_should_method
-
:should
-
end
-
end
-
-
# Wraps a matcher written against one of the legacy protocols in
-
# order to present the current protocol.
-
#
-
# @private
-
1
class LegacyMatcherAdapter < Matchers::MatcherDelegator
-
1
def initialize(matcher)
-
super
-
::RSpec.warn_deprecation(<<-EOS.gsub(/^\s+\|/, ''), :type => "legacy_matcher")
-
|#{matcher.class.name || matcher.inspect} implements a legacy RSpec matcher
-
|protocol. For the current protocol you should expose the failure messages
-
|via the `failure_message` and `failure_message_when_negated` methods.
-
|(Used from #{CallerFilter.first_non_rspec_line})
-
EOS
-
end
-
-
1
def self.wrap(matcher)
-
30
new(matcher) if interface_matches?(matcher)
-
end
-
-
# Starting in RSpec 1.2 (and continuing through all 2.x releases),
-
# the failure message protocol was:
-
# * `failure_message_for_should`
-
# * `failure_message_for_should_not`
-
# @private
-
1
class RSpec2 < self
-
1
def failure_message
-
base_matcher.failure_message_for_should
-
end
-
-
1
def failure_message_when_negated
-
base_matcher.failure_message_for_should_not
-
end
-
-
1
def self.interface_matches?(matcher)
-
(
-
!matcher.respond_to?(:failure_message) &&
-
15
matcher.respond_to?(:failure_message_for_should)
-
) || (
-
!matcher.respond_to?(:failure_message_when_negated) &&
-
15
matcher.respond_to?(:failure_message_for_should_not)
-
15
)
-
end
-
end
-
-
# Before RSpec 1.2, the failure message protocol was:
-
# * `failure_message`
-
# * `negative_failure_message`
-
# @private
-
1
class RSpec1 < self
-
1
def failure_message
-
base_matcher.failure_message
-
end
-
-
1
def failure_message_when_negated
-
base_matcher.negative_failure_message
-
end
-
-
# Note: `failure_message` is part of the RSpec 3 protocol
-
# (paired with `failure_message_when_negated`), so we don't check
-
# for `failure_message` here.
-
1
def self.interface_matches?(matcher)
-
!matcher.respond_to?(:failure_message_when_negated) &&
-
15
matcher.respond_to?(:negative_failure_message)
-
end
-
end
-
end
-
-
# RSpec 3.0 was released with the class name misspelled. For SemVer compatibility,
-
# we will provide this misspelled alias until 4.0.
-
# @deprecated Use LegacyMatcherAdapter instead.
-
# @private
-
1
LegacyMacherAdapter = LegacyMatcherAdapter
-
end
-
end
-
1
module RSpec
-
1
module Expectations
-
# @api private
-
# Provides methods for enabling and disabling the available
-
# syntaxes provided by rspec-expectations.
-
1
module Syntax
-
1
module_function
-
-
# @api private
-
# Determines where we add `should` and `should_not`.
-
1
def default_should_host
-
2
@default_should_host ||= ::Object.ancestors.last
-
end
-
-
# @api private
-
# Instructs rspec-expectations to warn on first usage of `should` or `should_not`.
-
# Enabled by default. This is largely here to facilitate testing.
-
1
def warn_about_should!
-
1
@warn_about_should = true
-
end
-
-
# @api private
-
# Generates a deprecation warning for the given method if no warning
-
# has already been issued.
-
1
def warn_about_should_unless_configured(method_name)
-
return unless @warn_about_should
-
-
RSpec.deprecate(
-
"Using `#{method_name}` from rspec-expectations' old `:should` syntax without explicitly enabling the syntax",
-
:replacement => "the new `:expect` syntax or explicitly enable `:should` with `config.expect_with(:rspec) { |c| c.syntax = :should }`"
-
)
-
-
@warn_about_should = false
-
end
-
-
# @api private
-
# Enables the `should` syntax.
-
1
def enable_should(syntax_host=default_should_host)
-
1
@warn_about_should = false if syntax_host == default_should_host
-
1
return if should_enabled?(syntax_host)
-
-
1
syntax_host.module_exec do
-
1
def should(matcher=nil, message=nil, &block)
-
::RSpec::Expectations::Syntax.warn_about_should_unless_configured(::Kernel.__method__)
-
::RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
-
end
-
-
1
def should_not(matcher=nil, message=nil, &block)
-
::RSpec::Expectations::Syntax.warn_about_should_unless_configured(::Kernel.__method__)
-
::RSpec::Expectations::NegativeExpectationHandler.handle_matcher(self, matcher, message, &block)
-
end
-
end
-
end
-
-
# @api private
-
# Disables the `should` syntax.
-
1
def disable_should(syntax_host=default_should_host)
-
return unless should_enabled?(syntax_host)
-
-
syntax_host.module_exec do
-
undef should
-
undef should_not
-
end
-
end
-
-
# @api private
-
# Enables the `expect` syntax.
-
1
def enable_expect(syntax_host=::RSpec::Matchers)
-
1
return if expect_enabled?(syntax_host)
-
-
1
syntax_host.module_exec do
-
1
def expect(value=::RSpec::Expectations::ExpectationTarget::UndefinedValue, &block)
-
15
::RSpec::Expectations::ExpectationTarget.for(value, block)
-
end
-
end
-
end
-
-
# @api private
-
# Disables the `expect` syntax.
-
1
def disable_expect(syntax_host=::RSpec::Matchers)
-
return unless expect_enabled?(syntax_host)
-
-
syntax_host.module_exec do
-
undef expect
-
end
-
end
-
-
# @api private
-
# Indicates whether or not the `should` syntax is enabled.
-
1
def should_enabled?(syntax_host=default_should_host)
-
1
syntax_host.method_defined?(:should)
-
end
-
-
# @api private
-
# Indicates whether or not the `expect` syntax is enabled.
-
1
def expect_enabled?(syntax_host=::RSpec::Matchers)
-
1
syntax_host.method_defined?(:expect)
-
end
-
end
-
end
-
end
-
-
1
if defined?(BasicObject)
-
# The legacy `:should` syntax adds the following methods directly to
-
# `BasicObject` so that they are available off of any object. Note, however,
-
# that this syntax does not always play nice with delegate/proxy objects.
-
# We recommend you use the non-monkeypatching `:expect` syntax instead.
-
1
class BasicObject
-
# @method should
-
# Passes if `matcher` returns true. Available on every `Object`.
-
# @example
-
# actual.should eq expected
-
# actual.should match /expression/
-
# @param [Matcher]
-
# matcher
-
# @param [String] message optional message to display when the expectation fails
-
# @return [Boolean] true if the expectation succeeds (else raises)
-
# @note This is only available when you have enabled the `:should` syntax.
-
# @see RSpec::Matchers
-
-
# @method should_not
-
# Passes if `matcher` returns false. Available on every `Object`.
-
# @example
-
# actual.should_not eq expected
-
# @param [Matcher]
-
# matcher
-
# @param [String] message optional message to display when the expectation fails
-
# @return [Boolean] false if the negative expectation succeeds (else raises)
-
# @note This is only available when you have enabled the `:should` syntax.
-
# @see RSpec::Matchers
-
end
-
end
-
1
module RSpec
-
1
module Expectations
-
# @private
-
1
module Version
-
1
STRING = '3.5.0'
-
end
-
end
-
end
-
1
require 'rspec/support'
-
1
RSpec::Support.require_rspec_support 'matcher_definition'
-
10
RSpec::Support.define_optimized_require_for_rspec(:matchers) { |f| require_relative(f) }
-
-
%w[
-
english_phrasing
-
composable
-
built_in
-
generated_descriptions
-
dsl
-
matcher_delegator
-
aliased_matcher
-
expecteds_for_multiple_diffs
-
9
].each { |file| RSpec::Support.require_rspec_matchers(file) }
-
-
# RSpec's top level namespace. All of rspec-expectations is contained
-
# in the `RSpec::Expectations` and `RSpec::Matchers` namespaces.
-
1
module RSpec
-
# RSpec::Matchers provides a number of useful matchers we use to define
-
# expectations. Any object that implements the [matcher protocol](Matchers/MatcherProtocol)
-
# can be used as a matcher.
-
#
-
# ## Predicates
-
#
-
# In addition to matchers that are defined explicitly, RSpec will create
-
# custom matchers on the fly for any arbitrary predicate, giving your specs a
-
# much more natural language feel.
-
#
-
# A Ruby predicate is a method that ends with a "?" and returns true or false.
-
# Common examples are `empty?`, `nil?`, and `instance_of?`.
-
#
-
# All you need to do is write `expect(..).to be_` followed by the predicate
-
# without the question mark, and RSpec will figure it out from there.
-
# For example:
-
#
-
# expect([]).to be_empty # => [].empty?() | passes
-
# expect([]).not_to be_empty # => [].empty?() | fails
-
#
-
# In addtion to prefixing the predicate matchers with "be_", you can also use "be_a_"
-
# and "be_an_", making your specs read much more naturally:
-
#
-
# expect("a string").to be_an_instance_of(String) # =>"a string".instance_of?(String) # passes
-
#
-
# expect(3).to be_a_kind_of(Fixnum) # => 3.kind_of?(Numeric) | passes
-
# expect(3).to be_a_kind_of(Numeric) # => 3.kind_of?(Numeric) | passes
-
# expect(3).to be_an_instance_of(Fixnum) # => 3.instance_of?(Fixnum) | passes
-
# expect(3).not_to be_an_instance_of(Numeric) # => 3.instance_of?(Numeric) | fails
-
#
-
# RSpec will also create custom matchers for predicates like `has_key?`. To
-
# use this feature, just state that the object should have_key(:key) and RSpec will
-
# call has_key?(:key) on the target. For example:
-
#
-
# expect(:a => "A").to have_key(:a)
-
# expect(:a => "A").to have_key(:b) # fails
-
#
-
# You can use this feature to invoke any predicate that begins with "has_", whether it is
-
# part of the Ruby libraries (like `Hash#has_key?`) or a method you wrote on your own class.
-
#
-
# Note that RSpec does not provide composable aliases for these dynamic predicate
-
# matchers. You can easily define your own aliases, though:
-
#
-
# RSpec::Matchers.alias_matcher :a_user_who_is_an_admin, :be_an_admin
-
# expect(user_list).to include(a_user_who_is_an_admin)
-
#
-
# ## Custom Matchers
-
#
-
# When you find that none of the stock matchers provide a natural feeling
-
# expectation, you can very easily write your own using RSpec's matcher DSL
-
# or writing one from scratch.
-
#
-
# ### Matcher DSL
-
#
-
# Imagine that you are writing a game in which players can be in various
-
# zones on a virtual board. To specify that bob should be in zone 4, you
-
# could say:
-
#
-
# expect(bob.current_zone).to eql(Zone.new("4"))
-
#
-
# But you might find it more expressive to say:
-
#
-
# expect(bob).to be_in_zone("4")
-
#
-
# and/or
-
#
-
# expect(bob).not_to be_in_zone("3")
-
#
-
# You can create such a matcher like so:
-
#
-
# RSpec::Matchers.define :be_in_zone do |zone|
-
# match do |player|
-
# player.in_zone?(zone)
-
# end
-
# end
-
#
-
# This will generate a <tt>be_in_zone</tt> method that returns a matcher
-
# with logical default messages for failures. You can override the failure
-
# messages and the generated description as follows:
-
#
-
# RSpec::Matchers.define :be_in_zone do |zone|
-
# match do |player|
-
# player.in_zone?(zone)
-
# end
-
#
-
# failure_message do |player|
-
# # generate and return the appropriate string.
-
# end
-
#
-
# failure_message_when_negated do |player|
-
# # generate and return the appropriate string.
-
# end
-
#
-
# description do
-
# # generate and return the appropriate string.
-
# end
-
# end
-
#
-
# Each of the message-generation methods has access to the block arguments
-
# passed to the <tt>create</tt> method (in this case, <tt>zone</tt>). The
-
# failure message methods (<tt>failure_message</tt> and
-
# <tt>failure_message_when_negated</tt>) are passed the actual value (the
-
# receiver of <tt>expect(..)</tt> or <tt>expect(..).not_to</tt>).
-
#
-
# ### Custom Matcher from scratch
-
#
-
# You could also write a custom matcher from scratch, as follows:
-
#
-
# class BeInZone
-
# def initialize(expected)
-
# @expected = expected
-
# end
-
#
-
# def matches?(target)
-
# @target = target
-
# @target.current_zone.eql?(Zone.new(@expected))
-
# end
-
#
-
# def failure_message
-
# "expected #{@target.inspect} to be in Zone #{@expected}"
-
# end
-
#
-
# def failure_message_when_negated
-
# "expected #{@target.inspect} not to be in Zone #{@expected}"
-
# end
-
# end
-
#
-
# ... and a method like this:
-
#
-
# def be_in_zone(expected)
-
# BeInZone.new(expected)
-
# end
-
#
-
# And then expose the method to your specs. This is normally done
-
# by including the method and the class in a module, which is then
-
# included in your spec:
-
#
-
# module CustomGameMatchers
-
# class BeInZone
-
# # ...
-
# end
-
#
-
# def be_in_zone(expected)
-
# # ...
-
# end
-
# end
-
#
-
# describe "Player behaviour" do
-
# include CustomGameMatchers
-
# # ...
-
# end
-
#
-
# or you can include in globally in a spec_helper.rb file <tt>require</tt>d
-
# from your spec file(s):
-
#
-
# RSpec::configure do |config|
-
# config.include(CustomGameMatchers)
-
# end
-
#
-
# ### Making custom matchers composable
-
#
-
# RSpec's built-in matchers are designed to be composed, in expressions like:
-
#
-
# expect(["barn", 2.45]).to contain_exactly(
-
# a_value_within(0.1).of(2.5),
-
# a_string_starting_with("bar")
-
# )
-
#
-
# Custom matchers can easily participate in composed matcher expressions like these.
-
# Include {RSpec::Matchers::Composable} in your custom matcher to make it support
-
# being composed (matchers defined using the DSL have this included automatically).
-
# Within your matcher's `matches?` method (or the `match` block, if using the DSL),
-
# use `values_match?(expected, actual)` rather than `expected == actual`.
-
# Under the covers, `values_match?` is able to match arbitrary
-
# nested data structures containing a mix of both matchers and non-matcher objects.
-
# It uses `===` and `==` to perform the matching, considering the values to
-
# match if either returns `true`. The `Composable` mixin also provides some helper
-
# methods for surfacing the matcher descriptions within your matcher's description
-
# or failure messages.
-
#
-
# RSpec's built-in matchers each have a number of aliases that rephrase the matcher
-
# from a verb phrase (such as `be_within`) to a noun phrase (such as `a_value_within`),
-
# which reads better when the matcher is passed as an argument in a composed matcher
-
# expressions, and also uses the noun-phrase wording in the matcher's `description`,
-
# for readable failure messages. You can alias your custom matchers in similar fashion
-
# using {RSpec::Matchers.alias_matcher}.
-
1
module Matchers
-
# @method expect
-
# Supports `expect(actual).to matcher` syntax by wrapping `actual` in an
-
# `ExpectationTarget`.
-
# @example
-
# expect(actual).to eq(expected)
-
# expect(actual).not_to eq(expected)
-
# @return [ExpectationTarget]
-
# @see ExpectationTarget#to
-
# @see ExpectationTarget#not_to
-
-
# Defines a matcher alias. The returned matcher's `description` will be overriden
-
# to reflect the phrasing of the new name, which will be used in failure messages
-
# when passed as an argument to another matcher in a composed matcher expression.
-
#
-
# @param new_name [Symbol] the new name for the matcher
-
# @param old_name [Symbol] the original name for the matcher
-
# @param options [Hash] options for the aliased matcher
-
# @option options [Class] :klass the ruby class to use as the decorator. (Not normally used).
-
# @yield [String] optional block that, when given, is used to define the overriden
-
# logic. The yielded arg is the original description or failure message. If no
-
# block is provided, a default override is used based on the old and new names.
-
#
-
# @example
-
# RSpec::Matchers.alias_matcher :a_list_that_sums_to, :sum_to
-
# sum_to(3).description # => "sum to 3"
-
# a_list_that_sums_to(3).description # => "a list that sums to 3"
-
#
-
# @example
-
# RSpec::Matchers.alias_matcher :a_list_sorted_by, :be_sorted_by do |description|
-
# description.sub("be sorted by", "a list sorted by")
-
# end
-
#
-
# be_sorted_by(:age).description # => "be sorted by age"
-
# a_list_sorted_by(:age).description # => "a list sorted by age"
-
#
-
# @!macro [attach] alias_matcher
-
# @!parse
-
# alias $1 $2
-
1
def self.alias_matcher(new_name, old_name, options={}, &description_override)
-
description_override ||= lambda do |old_desc|
-
old_desc.gsub(EnglishPhrasing.split_words(old_name), EnglishPhrasing.split_words(new_name))
-
58
end
-
115
klass = options.fetch(:klass) { AliasedMatcher }
-
-
58
define_method(new_name) do |*args, &block|
-
matcher = __send__(old_name, *args, &block)
-
klass.new(matcher, description_override)
-
end
-
end
-
-
# Defines a negated matcher. The returned matcher's `description` and `failure_message`
-
# will be overriden to reflect the phrasing of the new name, and the match logic will
-
# be based on the original matcher but negated.
-
#
-
# @param negated_name [Symbol] the name for the negated matcher
-
# @param base_name [Symbol] the name of the original matcher that will be negated
-
# @yield [String] optional block that, when given, is used to define the overriden
-
# logic. The yielded arg is the original description or failure message. If no
-
# block is provided, a default override is used based on the old and new names.
-
#
-
# @example
-
# RSpec::Matchers.define_negated_matcher :exclude, :include
-
# include(1, 2).description # => "include 1 and 2"
-
# exclude(1, 2).description # => "exclude 1 and 2"
-
#
-
# @note While the most obvious negated form may be to add a `not_` prefix,
-
# the failure messages you get with that form can be confusing (e.g.
-
# "expected [actual] to not [verb], but did not"). We've found it works
-
# best to find a more positive name for the negated form, such as
-
# `avoid_changing` rather than `not_change`.
-
1
def self.define_negated_matcher(negated_name, base_name, &description_override)
-
alias_matcher(negated_name, base_name, :klass => AliasedNegatedMatcher, &description_override)
-
end
-
-
# Allows multiple expectations in the provided block to fail, and then
-
# aggregates them into a single exception, rather than aborting on the
-
# first expectation failure like normal. This allows you to see all
-
# failures from an entire set of expectations without splitting each
-
# off into its own example (which may slow things down if the example
-
# setup is expensive).
-
#
-
# @param label [String] label for this aggregation block, which will be
-
# included in the aggregated exception message.
-
# @param metadata [Hash] additional metadata about this failure aggregation
-
# block. If multiple expectations fail, it will be exposed from the
-
# {Expectations::MultipleExpectationsNotMetError} exception. Mostly
-
# intended for internal RSpec use but you can use it as well.
-
# @yield Block containing as many expectation as you want. The block is
-
# simply yielded to, so you can trust that anything that works outside
-
# the block should work within it.
-
# @raise [Expectations::MultipleExpectationsNotMetError] raised when
-
# multiple expectations fail.
-
# @raise [Expectations::ExpectationNotMetError] raised when a single
-
# expectation fails.
-
# @raise [Exception] other sorts of exceptions will be raised as normal.
-
#
-
# @example
-
# aggregate_failures("verifying response") do
-
# expect(response.status).to eq(200)
-
# expect(response.headers).to include("Content-Type" => "text/plain")
-
# expect(response.body).to include("Success")
-
# end
-
#
-
# @note The implementation of this feature uses a thread-local variable,
-
# which means that if you have an expectation failure in another thread,
-
# it'll abort like normal.
-
1
def aggregate_failures(label=nil, metadata={}, &block)
-
Expectations::FailureAggregator.new(label, metadata).aggregate(&block)
-
end
-
-
# Passes if actual is truthy (anything but false or nil)
-
1
def be_truthy
-
BuiltIn::BeTruthy.new
-
end
-
1
alias_matcher :a_truthy_value, :be_truthy
-
-
# Passes if actual is falsey (false or nil)
-
1
def be_falsey
-
BuiltIn::BeFalsey.new
-
end
-
1
alias_matcher :be_falsy, :be_falsey
-
1
alias_matcher :a_falsey_value, :be_falsey
-
1
alias_matcher :a_falsy_value, :be_falsey
-
-
# Passes if actual is nil
-
1
def be_nil
-
BuiltIn::BeNil.new
-
end
-
1
alias_matcher :a_nil_value, :be_nil
-
-
# @example
-
# expect(actual).to be_truthy
-
# expect(actual).to be_falsey
-
# expect(actual).to be_nil
-
# expect(actual).to be_[arbitrary_predicate](*args)
-
# expect(actual).not_to be_nil
-
# expect(actual).not_to be_[arbitrary_predicate](*args)
-
#
-
# Given true, false, or nil, will pass if actual value is true, false or
-
# nil (respectively). Given no args means the caller should satisfy an if
-
# condition (to be or not to be).
-
#
-
# Predicates are any Ruby method that ends in a "?" and returns true or
-
# false. Given be_ followed by arbitrary_predicate (without the "?"),
-
# RSpec will match convert that into a query against the target object.
-
#
-
# The arbitrary_predicate feature will handle any predicate prefixed with
-
# "be_an_" (e.g. be_an_instance_of), "be_a_" (e.g. be_a_kind_of) or "be_"
-
# (e.g. be_empty), letting you choose the prefix that best suits the
-
# predicate.
-
1
def be(*args)
-
args.empty? ? Matchers::BuiltIn::Be.new : equal(*args)
-
end
-
1
alias_matcher :a_value, :be, :klass => AliasedMatcherWithOperatorSupport
-
-
# passes if target.kind_of?(klass)
-
1
def be_a(klass)
-
be_a_kind_of(klass)
-
end
-
1
alias_method :be_an, :be_a
-
-
# Passes if actual.instance_of?(expected)
-
#
-
# @example
-
# expect(5).to be_an_instance_of(Fixnum)
-
# expect(5).not_to be_an_instance_of(Numeric)
-
# expect(5).not_to be_an_instance_of(Float)
-
1
def be_an_instance_of(expected)
-
BuiltIn::BeAnInstanceOf.new(expected)
-
end
-
1
alias_method :be_instance_of, :be_an_instance_of
-
1
alias_matcher :an_instance_of, :be_an_instance_of
-
-
# Passes if actual.kind_of?(expected)
-
#
-
# @example
-
# expect(5).to be_a_kind_of(Fixnum)
-
# expect(5).to be_a_kind_of(Numeric)
-
# expect(5).not_to be_a_kind_of(Float)
-
1
def be_a_kind_of(expected)
-
BuiltIn::BeAKindOf.new(expected)
-
end
-
1
alias_method :be_kind_of, :be_a_kind_of
-
1
alias_matcher :a_kind_of, :be_a_kind_of
-
-
# Passes if actual.between?(min, max). Works with any Comparable object,
-
# including String, Symbol, Time, or Numeric (Fixnum, Bignum, Integer,
-
# Float, Complex, and Rational).
-
#
-
# By default, `be_between` is inclusive (i.e. passes when given either the max or min value),
-
# but you can make it `exclusive` by chaining that off the matcher.
-
#
-
# @example
-
# expect(5).to be_between(1, 10)
-
# expect(11).not_to be_between(1, 10)
-
# expect(10).not_to be_between(1, 10).exclusive
-
1
def be_between(min, max)
-
BuiltIn::BeBetween.new(min, max)
-
end
-
1
alias_matcher :a_value_between, :be_between
-
-
# Passes if actual == expected +/- delta
-
#
-
# @example
-
# expect(result).to be_within(0.5).of(3.0)
-
# expect(result).not_to be_within(0.5).of(3.0)
-
1
def be_within(delta)
-
BuiltIn::BeWithin.new(delta)
-
end
-
1
alias_matcher :a_value_within, :be_within
-
1
alias_matcher :within, :be_within
-
-
# Applied to a proc, specifies that its execution will cause some value to
-
# change.
-
#
-
# @param [Object] receiver
-
# @param [Symbol] message the message to send the receiver
-
#
-
# You can either pass <tt>receiver</tt> and <tt>message</tt>, or a block,
-
# but not both.
-
#
-
# When passing a block, it must use the `{ ... }` format, not
-
# do/end, as `{ ... }` binds to the `change` method, whereas do/end
-
# would errantly bind to the `expect(..).to` or `expect(...).not_to` method.
-
#
-
# You can chain any of the following off of the end to specify details
-
# about the change:
-
#
-
# * `from`
-
# * `to`
-
#
-
# or any one of:
-
#
-
# * `by`
-
# * `by_at_least`
-
# * `by_at_most`
-
#
-
# @example
-
# expect {
-
# team.add_player(player)
-
# }.to change(roster, :count)
-
#
-
# expect {
-
# team.add_player(player)
-
# }.to change(roster, :count).by(1)
-
#
-
# expect {
-
# team.add_player(player)
-
# }.to change(roster, :count).by_at_least(1)
-
#
-
# expect {
-
# team.add_player(player)
-
# }.to change(roster, :count).by_at_most(1)
-
#
-
# string = "string"
-
# expect {
-
# string.reverse!
-
# }.to change { string }.from("string").to("gnirts")
-
#
-
# string = "string"
-
# expect {
-
# string
-
# }.not_to change { string }.from("string")
-
#
-
# expect {
-
# person.happy_birthday
-
# }.to change(person, :birthday).from(32).to(33)
-
#
-
# expect {
-
# employee.develop_great_new_social_networking_app
-
# }.to change(employee, :title).from("Mail Clerk").to("CEO")
-
#
-
# expect {
-
# doctor.leave_office
-
# }.to change(doctor, :sign).from(/is in/).to(/is out/)
-
#
-
# user = User.new(:type => "admin")
-
# expect {
-
# user.symbolize_type
-
# }.to change(user, :type).from(String).to(Symbol)
-
#
-
# == Notes
-
#
-
# Evaluates `receiver.message` or `block` before and after it
-
# evaluates the block passed to `expect`.
-
#
-
# `expect( ... ).not_to change` supports the form that specifies `from`
-
# (which specifies what you expect the starting, unchanged value to be)
-
# but does not support forms with subsequent calls to `by`, `by_at_least`,
-
# `by_at_most` or `to`.
-
1
def change(receiver=nil, message=nil, &block)
-
6
BuiltIn::Change.new(receiver, message, &block)
-
end
-
1
alias_matcher :a_block_changing, :change
-
1
alias_matcher :changing, :change
-
-
# Passes if actual contains all of the expected regardless of order.
-
# This works for collections. Pass in multiple args and it will only
-
# pass if all args are found in collection.
-
#
-
# @note This is also available using the `=~` operator with `should`,
-
# but `=~` is not supported with `expect`.
-
#
-
# @example
-
# expect([1, 2, 3]).to contain_exactly(1, 2, 3)
-
# expect([1, 2, 3]).to contain_exactly(1, 3, 2)
-
#
-
# @see #match_array
-
1
def contain_exactly(*items)
-
BuiltIn::ContainExactly.new(items)
-
end
-
1
alias_matcher :a_collection_containing_exactly, :contain_exactly
-
1
alias_matcher :containing_exactly, :contain_exactly
-
-
# Passes if actual covers expected. This works for
-
# Ranges. You can also pass in multiple args
-
# and it will only pass if all args are found in Range.
-
#
-
# @example
-
# expect(1..10).to cover(5)
-
# expect(1..10).to cover(4, 6)
-
# expect(1..10).to cover(4, 6, 11) # fails
-
# expect(1..10).not_to cover(11)
-
# expect(1..10).not_to cover(5) # fails
-
#
-
# ### Warning:: Ruby >= 1.9 only
-
1
def cover(*values)
-
BuiltIn::Cover.new(*values)
-
end
-
1
alias_matcher :a_range_covering, :cover
-
1
alias_matcher :covering, :cover
-
-
# Matches if the actual value ends with the expected value(s). In the case
-
# of a string, matches against the last `expected.length` characters of the
-
# actual string. In the case of an array, matches against the last
-
# `expected.length` elements of the actual array.
-
#
-
# @example
-
# expect("this string").to end_with "string"
-
# expect([0, 1, 2, 3, 4]).to end_with 4
-
# expect([0, 2, 3, 4, 4]).to end_with 3, 4
-
1
def end_with(*expected)
-
BuiltIn::EndWith.new(*expected)
-
end
-
1
alias_matcher :a_collection_ending_with, :end_with
-
1
alias_matcher :a_string_ending_with, :end_with
-
1
alias_matcher :ending_with, :end_with
-
-
# Passes if <tt>actual == expected</tt>.
-
#
-
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more
-
# information about equality in Ruby.
-
#
-
# @example
-
# expect(5).to eq(5)
-
# expect(5).not_to eq(3)
-
1
def eq(expected)
-
6
BuiltIn::Eq.new(expected)
-
end
-
1
alias_matcher :an_object_eq_to, :eq
-
1
alias_matcher :eq_to, :eq
-
-
# Passes if `actual.eql?(expected)`
-
#
-
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more
-
# information about equality in Ruby.
-
#
-
# @example
-
# expect(5).to eql(5)
-
# expect(5).not_to eql(3)
-
1
def eql(expected)
-
BuiltIn::Eql.new(expected)
-
end
-
1
alias_matcher :an_object_eql_to, :eql
-
1
alias_matcher :eql_to, :eql
-
-
# Passes if <tt>actual.equal?(expected)</tt> (object identity).
-
#
-
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more
-
# information about equality in Ruby.
-
#
-
# @example
-
# expect(5).to equal(5) # Fixnums are equal
-
# expect("5").not_to equal("5") # Strings that look the same are not the same object
-
1
def equal(expected)
-
BuiltIn::Equal.new(expected)
-
end
-
1
alias_matcher :an_object_equal_to, :equal
-
1
alias_matcher :equal_to, :equal
-
-
# Passes if `actual.exist?` or `actual.exists?`
-
#
-
# @example
-
# expect(File).to exist("path/to/file")
-
1
def exist(*args)
-
BuiltIn::Exist.new(*args)
-
end
-
1
alias_matcher :an_object_existing, :exist
-
1
alias_matcher :existing, :exist
-
-
# Passes if actual's attribute values match the expected attributes hash.
-
# This works no matter how you define your attribute readers.
-
#
-
# @example
-
# Person = Struct.new(:name, :age)
-
# person = Person.new("Bob", 32)
-
#
-
# expect(person).to have_attributes(:name => "Bob", :age => 32)
-
# expect(person).to have_attributes(:name => a_string_starting_with("B"), :age => (a_value > 30) )
-
#
-
# @note It will fail if actual doesn't respond to any of the expected attributes.
-
#
-
# @example
-
# expect(person).to have_attributes(:color => "red")
-
1
def have_attributes(expected)
-
BuiltIn::HaveAttributes.new(expected)
-
end
-
1
alias_matcher :an_object_having_attributes, :have_attributes
-
1
alias_matcher :having_attributes, :have_attributes
-
-
# Passes if actual includes expected. This works for
-
# collections and Strings. You can also pass in multiple args
-
# and it will only pass if all args are found in collection.
-
#
-
# @example
-
# expect([1,2,3]).to include(3)
-
# expect([1,2,3]).to include(2,3)
-
# expect([1,2,3]).to include(2,3,4) # fails
-
# expect([1,2,3]).not_to include(4)
-
# expect("spread").to include("read")
-
# expect("spread").not_to include("red")
-
# expect(:a => 1, :b => 2).to include(:a)
-
# expect(:a => 1, :b => 2).to include(:a, :b)
-
# expect(:a => 1, :b => 2).to include(:a => 1)
-
# expect(:a => 1, :b => 2).to include(:b => 2, :a => 1)
-
# expect(:a => 1, :b => 2).to include(:c) # fails
-
# expect(:a => 1, :b => 2).not_to include(:a => 2)
-
1
def include(*expected)
-
BuiltIn::Include.new(*expected)
-
end
-
1
alias_matcher :a_collection_including, :include
-
1
alias_matcher :a_string_including, :include
-
1
alias_matcher :a_hash_including, :include
-
1
alias_matcher :including, :include
-
-
# Passes if the provided matcher passes when checked against all
-
# elements of the collection.
-
#
-
# @example
-
# expect([1, 3, 5]).to all be_odd
-
# expect([1, 3, 6]).to all be_odd # fails
-
#
-
# @note The negative form `not_to all` is not supported. Instead
-
# use `not_to include` or pass a negative form of a matcher
-
# as the argument (e.g. `all exclude(:foo)`).
-
#
-
# @note You can also use this with compound matchers as well.
-
#
-
# @example
-
# expect([1, 3, 5]).to all( be_odd.and be_an(Integer) )
-
1
def all(expected)
-
BuiltIn::All.new(expected)
-
end
-
-
# Given a `Regexp` or `String`, passes if `actual.match(pattern)`
-
# Given an arbitrary nested data structure (e.g. arrays and hashes),
-
# matches if `expected === actual` || `actual == expected` for each
-
# pair of elements.
-
#
-
# @example
-
# expect(email).to match(/^([^\s]+)((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
-
# expect(email).to match("@example.com")
-
#
-
# @example
-
# hash = {
-
# :a => {
-
# :b => ["foo", 5],
-
# :c => { :d => 2.05 }
-
# }
-
# }
-
#
-
# expect(hash).to match(
-
# :a => {
-
# :b => a_collection_containing_exactly(
-
# a_string_starting_with("f"),
-
# an_instance_of(Fixnum)
-
# ),
-
# :c => { :d => (a_value < 3) }
-
# }
-
# )
-
#
-
# @note The `match_regex` alias is deprecated and is not recommended for use.
-
# It was added in 2.12.1 to facilitate its use from within custom
-
# matchers (due to how the custom matcher DSL was evaluated in 2.x,
-
# `match` could not be used there), but is no longer needed in 3.x.
-
1
def match(expected)
-
BuiltIn::Match.new(expected)
-
end
-
1
alias_matcher :match_regex, :match
-
1
alias_matcher :an_object_matching, :match
-
1
alias_matcher :a_string_matching, :match
-
1
alias_matcher :matching, :match
-
-
# An alternate form of `contain_exactly` that accepts
-
# the expected contents as a single array arg rather
-
# that splatted out as individual items.
-
#
-
# @example
-
# expect(results).to contain_exactly(1, 2)
-
# # is identical to:
-
# expect(results).to match_array([1, 2])
-
#
-
# @see #contain_exactly
-
1
def match_array(items)
-
contain_exactly(*items)
-
end
-
-
# With no arg, passes if the block outputs `to_stdout` or `to_stderr`.
-
# With a string, passes if the block outputs that specific string `to_stdout` or `to_stderr`.
-
# With a regexp or matcher, passes if the block outputs a string `to_stdout` or `to_stderr` that matches.
-
#
-
# To capture output from any spawned subprocess as well, use `to_stdout_from_any_process` or
-
# `to_stderr_from_any_process`. Output from any process that inherits the main process's corresponding
-
# standard stream will be captured.
-
#
-
# @example
-
# expect { print 'foo' }.to output.to_stdout
-
# expect { print 'foo' }.to output('foo').to_stdout
-
# expect { print 'foo' }.to output(/foo/).to_stdout
-
#
-
# expect { do_something }.to_not output.to_stdout
-
#
-
# expect { warn('foo') }.to output.to_stderr
-
# expect { warn('foo') }.to output('foo').to_stderr
-
# expect { warn('foo') }.to output(/foo/).to_stderr
-
#
-
# expect { do_something }.to_not output.to_stderr
-
#
-
# expect { system('echo foo') }.to output("foo\n").to_stdout_from_any_process
-
# expect { system('echo foo', out: :err) }.to output("foo\n").to_stderr_from_any_process
-
#
-
# @note `to_stdout` and `to_stderr` work by temporarily replacing `$stdout` or `$stderr`,
-
# so they're not able to intercept stream output that explicitly uses `STDOUT`/`STDERR`
-
# or that uses a reference to `$stdout`/`$stderr` that was stored before the
-
# matcher was used.
-
# @note `to_stdout_from_any_process` and `to_stderr_from_any_process` use Tempfiles, and
-
# are thus significantly (~30x) slower than `to_stdout` and `to_stderr`.
-
1
def output(expected=nil)
-
BuiltIn::Output.new(expected)
-
end
-
1
alias_matcher :a_block_outputting, :output
-
-
# With no args, matches if any error is raised.
-
# With a named error, matches only if that specific error is raised.
-
# With a named error and messsage specified as a String, matches only if both match.
-
# With a named error and messsage specified as a Regexp, matches only if both match.
-
# Pass an optional block to perform extra verifications on the exception matched
-
#
-
# @example
-
# expect { do_something_risky }.to raise_error
-
# expect { do_something_risky }.to raise_error(PoorRiskDecisionError)
-
# expect { do_something_risky }.to raise_error(PoorRiskDecisionError) { |error| expect(error.data).to eq 42 }
-
# expect { do_something_risky }.to raise_error(PoorRiskDecisionError, "that was too risky")
-
# expect { do_something_risky }.to raise_error(PoorRiskDecisionError, /oo ri/)
-
#
-
# expect { do_something_risky }.not_to raise_error
-
1
def raise_error(error=nil, message=nil, &block)
-
BuiltIn::RaiseError.new(error, message, &block)
-
end
-
1
alias_method :raise_exception, :raise_error
-
-
1
alias_matcher :a_block_raising, :raise_error do |desc|
-
desc.sub("raise", "a block raising")
-
end
-
-
1
alias_matcher :raising, :raise_error do |desc|
-
desc.sub("raise", "raising")
-
end
-
-
# Matches if the target object responds to all of the names
-
# provided. Names can be Strings or Symbols.
-
#
-
# @example
-
# expect("string").to respond_to(:length)
-
#
-
1
def respond_to(*names)
-
BuiltIn::RespondTo.new(*names)
-
end
-
1
alias_matcher :an_object_responding_to, :respond_to
-
1
alias_matcher :responding_to, :respond_to
-
-
# Passes if the submitted block returns true. Yields target to the
-
# block.
-
#
-
# Generally speaking, this should be thought of as a last resort when
-
# you can't find any other way to specify the behaviour you wish to
-
# specify.
-
#
-
# If you do find yourself in such a situation, you could always write
-
# a custom matcher, which would likely make your specs more expressive.
-
#
-
# @param description [String] optional description to be used for this matcher.
-
#
-
# @example
-
# expect(5).to satisfy { |n| n > 3 }
-
# expect(5).to satisfy("be greater than 3") { |n| n > 3 }
-
1
def satisfy(description="satisfy block", &block)
-
BuiltIn::Satisfy.new(description, &block)
-
end
-
1
alias_matcher :an_object_satisfying, :satisfy
-
1
alias_matcher :satisfying, :satisfy
-
-
# Matches if the actual value starts with the expected value(s). In the
-
# case of a string, matches against the first `expected.length` characters
-
# of the actual string. In the case of an array, matches against the first
-
# `expected.length` elements of the actual array.
-
#
-
# @example
-
# expect("this string").to start_with "this s"
-
# expect([0, 1, 2, 3, 4]).to start_with 0
-
# expect([0, 2, 3, 4, 4]).to start_with 0, 1
-
1
def start_with(*expected)
-
BuiltIn::StartWith.new(*expected)
-
end
-
1
alias_matcher :a_collection_starting_with, :start_with
-
1
alias_matcher :a_string_starting_with, :start_with
-
1
alias_matcher :starting_with, :start_with
-
-
# Given no argument, matches if a proc throws any Symbol.
-
#
-
# Given a Symbol, matches if the given proc throws the specified Symbol.
-
#
-
# Given a Symbol and an arg, matches if the given proc throws the
-
# specified Symbol with the specified arg.
-
#
-
# @example
-
# expect { do_something_risky }.to throw_symbol
-
# expect { do_something_risky }.to throw_symbol(:that_was_risky)
-
# expect { do_something_risky }.to throw_symbol(:that_was_risky, 'culprit')
-
#
-
# expect { do_something_risky }.not_to throw_symbol
-
# expect { do_something_risky }.not_to throw_symbol(:that_was_risky)
-
# expect { do_something_risky }.not_to throw_symbol(:that_was_risky, 'culprit')
-
1
def throw_symbol(expected_symbol=nil, expected_arg=nil)
-
BuiltIn::ThrowSymbol.new(expected_symbol, expected_arg)
-
end
-
-
1
alias_matcher :a_block_throwing, :throw_symbol do |desc|
-
desc.sub("throw", "a block throwing")
-
end
-
-
1
alias_matcher :throwing, :throw_symbol do |desc|
-
desc.sub("throw", "throwing")
-
end
-
-
# Passes if the method called in the expect block yields, regardless
-
# of whether or not arguments are yielded.
-
#
-
# @example
-
# expect { |b| 5.tap(&b) }.to yield_control
-
# expect { |b| "a".to_sym(&b) }.not_to yield_control
-
#
-
# @note Your expect block must accept a parameter and pass it on to
-
# the method-under-test as a block.
-
1
def yield_control
-
BuiltIn::YieldControl.new
-
end
-
1
alias_matcher :a_block_yielding_control, :yield_control
-
1
alias_matcher :yielding_control, :yield_control
-
-
# Passes if the method called in the expect block yields with
-
# no arguments. Fails if it does not yield, or yields with arguments.
-
#
-
# @example
-
# expect { |b| User.transaction(&b) }.to yield_with_no_args
-
# expect { |b| 5.tap(&b) }.not_to yield_with_no_args # because it yields with `5`
-
# expect { |b| "a".to_sym(&b) }.not_to yield_with_no_args # because it does not yield
-
#
-
# @note Your expect block must accept a parameter and pass it on to
-
# the method-under-test as a block.
-
# @note This matcher is not designed for use with methods that yield
-
# multiple times.
-
1
def yield_with_no_args
-
BuiltIn::YieldWithNoArgs.new
-
end
-
1
alias_matcher :a_block_yielding_with_no_args, :yield_with_no_args
-
1
alias_matcher :yielding_with_no_args, :yield_with_no_args
-
-
# Given no arguments, matches if the method called in the expect
-
# block yields with arguments (regardless of what they are or how
-
# many there are).
-
#
-
# Given arguments, matches if the method called in the expect block
-
# yields with arguments that match the given arguments.
-
#
-
# Argument matching is done using `===` (the case match operator)
-
# and `==`. If the expected and actual arguments match with either
-
# operator, the matcher will pass.
-
#
-
# @example
-
# expect { |b| 5.tap(&b) }.to yield_with_args # because #tap yields an arg
-
# expect { |b| 5.tap(&b) }.to yield_with_args(5) # because 5 == 5
-
# expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum) # because Fixnum === 5
-
# expect { |b| File.open("f.txt", &b) }.to yield_with_args(/txt/) # because /txt/ === "f.txt"
-
#
-
# expect { |b| User.transaction(&b) }.not_to yield_with_args # because it yields no args
-
# expect { |b| 5.tap(&b) }.not_to yield_with_args(1, 2, 3)
-
#
-
# @note Your expect block must accept a parameter and pass it on to
-
# the method-under-test as a block.
-
# @note This matcher is not designed for use with methods that yield
-
# multiple times.
-
1
def yield_with_args(*args)
-
BuiltIn::YieldWithArgs.new(*args)
-
end
-
1
alias_matcher :a_block_yielding_with_args, :yield_with_args
-
1
alias_matcher :yielding_with_args, :yield_with_args
-
-
# Designed for use with methods that repeatedly yield (such as
-
# iterators). Passes if the method called in the expect block yields
-
# multiple times with arguments matching those given.
-
#
-
# Argument matching is done using `===` (the case match operator)
-
# and `==`. If the expected and actual arguments match with either
-
# operator, the matcher will pass.
-
#
-
# @example
-
# expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
-
# expect { |b| { :a => 1, :b => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
-
# expect { |b| [1, 2, 3].each(&b) }.not_to yield_successive_args(1, 2)
-
#
-
# @note Your expect block must accept a parameter and pass it on to
-
# the method-under-test as a block.
-
1
def yield_successive_args(*args)
-
BuiltIn::YieldSuccessiveArgs.new(*args)
-
end
-
1
alias_matcher :a_block_yielding_successive_args, :yield_successive_args
-
1
alias_matcher :yielding_successive_args, :yield_successive_args
-
-
# Delegates to {RSpec::Expectations.configuration}.
-
# This is here because rspec-core's `expect_with` option
-
# looks for a `configuration` method on the mixin
-
# (`RSpec::Matchers`) to yield to a block.
-
# @return [RSpec::Expectations::Configuration] the configuration object
-
1
def self.configuration
-
1
Expectations.configuration
-
end
-
-
1
private
-
-
1
BE_PREDICATE_REGEX = /^(be_(?:an?_)?)(.*)/
-
1
HAS_REGEX = /^(?:have_)(.*)/
-
1
DYNAMIC_MATCHER_REGEX = Regexp.union(BE_PREDICATE_REGEX, HAS_REGEX)
-
-
1
def method_missing(method, *args, &block)
-
case method.to_s
-
when BE_PREDICATE_REGEX
-
BuiltIn::BePredicate.new(method, *args, &block)
-
when HAS_REGEX
-
BuiltIn::Has.new(method, *args, &block)
-
else
-
super
-
end
-
end
-
-
1
if RUBY_VERSION.to_f >= 1.9
-
1
def respond_to_missing?(method, *)
-
method =~ DYNAMIC_MATCHER_REGEX || super
-
end
-
else # for 1.8.7
-
# :nocov:
-
skipped
def respond_to?(method, *)
-
skipped
method = method.to_s
-
skipped
method =~ DYNAMIC_MATCHER_REGEX || super
-
skipped
end
-
skipped
public :respond_to?
-
# :nocov:
-
end
-
-
# @api private
-
1
def self.is_a_matcher?(obj)
-
2
return true if ::RSpec::Matchers::BuiltIn::BaseMatcher === obj
-
2
begin
-
2
return false if obj.respond_to?(:i_respond_to_everything_so_im_not_really_a_matcher)
-
rescue NoMethodError
-
# Some objects, like BasicObject, don't implemented standard
-
# reflection methods.
-
return false
-
end
-
2
return false unless obj.respond_to?(:matches?)
-
-
obj.respond_to?(:failure_message) ||
-
obj.respond_to?(:failure_message_for_should) # support legacy matchers
-
end
-
-
1
::RSpec::Support.register_matcher_definition do |obj|
-
is_a_matcher?(obj)
-
end
-
-
# @api private
-
1
def self.is_a_describable_matcher?(obj)
-
is_a_matcher?(obj) && obj.respond_to?(:description)
-
end
-
-
1
if RSpec::Support::Ruby.mri? && RUBY_VERSION[0, 3] == '1.9'
-
# @api private
-
# Note that `included` doesn't work for this because it is triggered
-
# _after_ `RSpec::Matchers` is an ancestor of the inclusion host, rather
-
# than _before_, like `append_features`. It's important we check this before
-
# in order to find the cases where it was already previously included.
-
def self.append_features(mod)
-
return super if mod < self # `mod < self` indicates a re-inclusion.
-
-
subclasses = ObjectSpace.each_object(Class).select { |c| c < mod && c < self }
-
return super unless subclasses.any?
-
-
subclasses.reject! { |s| subclasses.any? { |s2| s < s2 } } # Filter to the root ancestor.
-
subclasses = subclasses.map { |s| "`#{s}`" }.join(", ")
-
-
RSpec.warning "`#{self}` has been included in a superclass (`#{mod}`) " \
-
"after previously being included in subclasses (#{subclasses}), " \
-
"which can trigger infinite recursion from `super` due to an MRI 1.9 bug " \
-
"(https://redmine.ruby-lang.org/issues/3351). To work around this, " \
-
"either upgrade to MRI 2.0+, include a dup of the module (e.g. " \
-
"`include #{self}.dup`), or find a way to include `#{self}` in `#{mod}` " \
-
"before it is included in subclasses (#{subclasses}). See " \
-
"https://github.com/rspec/rspec-expectations/issues/814 for more info"
-
-
super
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
# Decorator that wraps a matcher and overrides `description`
-
# using the provided block in order to support an alias
-
# of a matcher. This is intended for use when composing
-
# matchers, so that you can use an expression like
-
# `include( a_value_within(0.1).of(3) )` rather than
-
# `include( be_within(0.1).of(3) )`, and have the corresponding
-
# description read naturally.
-
#
-
# @api private
-
1
class AliasedMatcher < MatcherDelegator
-
1
def initialize(base_matcher, description_block)
-
@description_block = description_block
-
super(base_matcher)
-
end
-
-
# Forward messages on to the wrapped matcher.
-
# Since many matchers provide a fluent interface
-
# (e.g. `a_value_within(0.1).of(3)`), we need to wrap
-
# the returned value if it responds to `description`,
-
# so that our override can be applied when it is eventually
-
# used.
-
1
def method_missing(*)
-
return_val = super
-
return return_val unless RSpec::Matchers.is_a_matcher?(return_val)
-
self.class.new(return_val, @description_block)
-
end
-
-
# Provides the description of the aliased matcher. Aliased matchers
-
# are designed to behave identically to the original matcher except
-
# for the description and failure messages. The description is different
-
# to reflect the aliased name.
-
#
-
# @api private
-
1
def description
-
@description_block.call(super)
-
end
-
-
# Provides the failure_message of the aliased matcher. Aliased matchers
-
# are designed to behave identically to the original matcher except
-
# for the description and failure messages. The failure_message is different
-
# to reflect the aliased name.
-
#
-
# @api private
-
1
def failure_message
-
@description_block.call(super)
-
end
-
-
# Provides the failure_message_when_negated of the aliased matcher. Aliased matchers
-
# are designed to behave identically to the original matcher except
-
# for the description and failure messages. The failure_message_when_negated is different
-
# to reflect the aliased name.
-
#
-
# @api private
-
1
def failure_message_when_negated
-
@description_block.call(super)
-
end
-
end
-
-
# Decorator used for matchers that have special implementations of
-
# operators like `==` and `===`.
-
# @private
-
1
class AliasedMatcherWithOperatorSupport < AliasedMatcher
-
# We undef these so that they get delegated via `method_missing`.
-
1
undef ==
-
1
undef ===
-
end
-
-
# @private
-
1
class AliasedNegatedMatcher < AliasedMatcher
-
1
def matches?(*args, &block)
-
if @base_matcher.respond_to?(:does_not_match?)
-
@base_matcher.does_not_match?(*args, &block)
-
else
-
!super
-
end
-
end
-
-
1
def does_not_match?(*args, &block)
-
@base_matcher.matches?(*args, &block)
-
end
-
-
1
def failure_message
-
optimal_failure_message(__method__, :failure_message_when_negated)
-
end
-
-
1
def failure_message_when_negated
-
optimal_failure_message(__method__, :failure_message)
-
end
-
-
1
private
-
-
1
DefaultFailureMessages = BuiltIn::BaseMatcher::DefaultFailureMessages
-
-
# For a matcher that uses the default failure messages, we prefer to
-
# use the override provided by the `description_block`, because it
-
# includes the phrasing that the user has expressed a preference for
-
# by going through the effort of defining a negated matcher.
-
#
-
# However, if the override didn't actually change anything, then we
-
# should return the opposite failure message instead -- the overriden
-
# message is going to be confusing if we return it as-is, as it represents
-
# the non-negated failure message for a negated match (or vice versa).
-
1
def optimal_failure_message(same, inverted)
-
if DefaultFailureMessages.has_default_failure_messages?(@base_matcher)
-
base_message = @base_matcher.__send__(same)
-
overriden = @description_block.call(base_message)
-
return overriden if overriden != base_message
-
end
-
-
@base_matcher.__send__(inverted)
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_matchers "built_in/base_matcher"
-
-
1
module RSpec
-
1
module Matchers
-
# Container module for all built-in matchers. The matcher classes are here
-
# (rather than directly under `RSpec::Matchers`) in order to prevent name
-
# collisions, since `RSpec::Matchers` gets included into the user's namespace.
-
#
-
# Autoloading is used to delay when the matcher classes get loaded, allowing
-
# rspec-matchers to boot faster, and avoiding loading matchers the user is
-
# not using.
-
1
module BuiltIn
-
1
autoload :BeAKindOf, 'rspec/matchers/built_in/be_kind_of'
-
1
autoload :BeAnInstanceOf, 'rspec/matchers/built_in/be_instance_of'
-
1
autoload :BeBetween, 'rspec/matchers/built_in/be_between'
-
1
autoload :Be, 'rspec/matchers/built_in/be'
-
1
autoload :BeComparedTo, 'rspec/matchers/built_in/be'
-
1
autoload :BeFalsey, 'rspec/matchers/built_in/be'
-
1
autoload :BeNil, 'rspec/matchers/built_in/be'
-
1
autoload :BePredicate, 'rspec/matchers/built_in/be'
-
1
autoload :BeTruthy, 'rspec/matchers/built_in/be'
-
1
autoload :BeWithin, 'rspec/matchers/built_in/be_within'
-
1
autoload :Change, 'rspec/matchers/built_in/change'
-
1
autoload :Compound, 'rspec/matchers/built_in/compound'
-
1
autoload :ContainExactly, 'rspec/matchers/built_in/contain_exactly'
-
1
autoload :Cover, 'rspec/matchers/built_in/cover'
-
1
autoload :EndWith, 'rspec/matchers/built_in/start_or_end_with'
-
1
autoload :Eq, 'rspec/matchers/built_in/eq'
-
1
autoload :Eql, 'rspec/matchers/built_in/eql'
-
1
autoload :Equal, 'rspec/matchers/built_in/equal'
-
1
autoload :Exist, 'rspec/matchers/built_in/exist'
-
1
autoload :Has, 'rspec/matchers/built_in/has'
-
1
autoload :HaveAttributes, 'rspec/matchers/built_in/have_attributes'
-
1
autoload :Include, 'rspec/matchers/built_in/include'
-
1
autoload :All, 'rspec/matchers/built_in/all'
-
1
autoload :Match, 'rspec/matchers/built_in/match'
-
1
autoload :NegativeOperatorMatcher, 'rspec/matchers/built_in/operators'
-
1
autoload :OperatorMatcher, 'rspec/matchers/built_in/operators'
-
1
autoload :Output, 'rspec/matchers/built_in/output'
-
1
autoload :PositiveOperatorMatcher, 'rspec/matchers/built_in/operators'
-
1
autoload :RaiseError, 'rspec/matchers/built_in/raise_error'
-
1
autoload :RespondTo, 'rspec/matchers/built_in/respond_to'
-
1
autoload :Satisfy, 'rspec/matchers/built_in/satisfy'
-
1
autoload :StartWith, 'rspec/matchers/built_in/start_or_end_with'
-
1
autoload :ThrowSymbol, 'rspec/matchers/built_in/throw_symbol'
-
1
autoload :YieldControl, 'rspec/matchers/built_in/yield'
-
1
autoload :YieldSuccessiveArgs, 'rspec/matchers/built_in/yield'
-
1
autoload :YieldWithArgs, 'rspec/matchers/built_in/yield'
-
1
autoload :YieldWithNoArgs, 'rspec/matchers/built_in/yield'
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
#
-
# Used _internally_ as a base class for matchers that ship with
-
# rspec-expectations and rspec-rails.
-
#
-
# ### Warning:
-
#
-
# This class is for internal use, and subject to change without notice.
-
# We strongly recommend that you do not base your custom matchers on this
-
# class. If/when this changes, we will announce it and remove this warning.
-
1
class BaseMatcher
-
1
include RSpec::Matchers::Composable
-
-
# @api private
-
# Used to detect when no arg is passed to `initialize`.
-
# `nil` cannot be used because it's a valid value to pass.
-
1
UNDEFINED = Object.new.freeze
-
-
# @private
-
1
attr_reader :actual, :expected, :rescued_exception
-
-
1
def initialize(expected=UNDEFINED)
-
6
@expected = expected unless UNDEFINED.equal?(expected)
-
end
-
-
# @api private
-
# Indicates if the match is successful. Delegates to `match`, which
-
# should be defined on a subclass. Takes care of consistently
-
# initializing the `actual` attribute.
-
1
def matches?(actual)
-
6
@actual = actual
-
6
match(expected, actual)
-
end
-
-
# @api private
-
# Used to wrap a block of code that will indicate failure by
-
# raising one of the named exceptions.
-
#
-
# This is used by rspec-rails for some of its matchers that
-
# wrap rails' assertions.
-
1
def match_unless_raises(*exceptions)
-
exceptions.unshift Exception if exceptions.empty?
-
begin
-
yield
-
true
-
rescue *exceptions => @rescued_exception
-
false
-
end
-
end
-
-
# @api private
-
# Generates a description using {EnglishPhrasing}.
-
# @return [String]
-
1
def description
-
desc = EnglishPhrasing.split_words(self.class.matcher_name)
-
desc << EnglishPhrasing.list(@expected) if defined?(@expected)
-
desc
-
end
-
-
# @api private
-
# Matchers are not diffable by default. Override this to make your
-
# subclass diffable.
-
1
def diffable?
-
false
-
end
-
-
# @api private
-
# Most matchers are value matchers (i.e. meant to work with `expect(value)`)
-
# rather than block matchers (i.e. meant to work with `expect { }`), so
-
# this defaults to false. Block matchers must override this to return true.
-
1
def supports_block_expectations?
-
false
-
end
-
-
# @api private
-
1
def expects_call_stack_jump?
-
false
-
end
-
-
# @private
-
1
def expected_formatted
-
RSpec::Support::ObjectFormatter.format(@expected)
-
end
-
-
# @private
-
1
def actual_formatted
-
RSpec::Support::ObjectFormatter.format(@actual)
-
end
-
-
# @private
-
1
def self.matcher_name
-
@matcher_name ||= underscore(name.split('::').last)
-
end
-
-
# @private
-
# Borrowed from ActiveSupport.
-
1
def self.underscore(camel_cased_word)
-
word = camel_cased_word.to_s.dup
-
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
-
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
-
word.tr!('-', '_')
-
word.downcase!
-
word
-
end
-
1
private_class_method :underscore
-
-
1
private
-
-
1
def assert_ivars(*expected_ivars)
-
return unless (expected_ivars - present_ivars).any?
-
ivar_list = EnglishPhrasing.list(expected_ivars)
-
raise "#{self.class.name} needs to supply#{ivar_list}"
-
end
-
-
1
if RUBY_VERSION.to_f < 1.9
-
# :nocov:
-
skipped
def present_ivars
-
skipped
instance_variables.map(&:to_sym)
-
skipped
end
-
# :nocov:
-
else
-
1
alias present_ivars instance_variables
-
end
-
-
# @private
-
1
module HashFormatting
-
# `{ :a => 5, :b => 2 }.inspect` produces:
-
#
-
# {:a=>5, :b=>2}
-
#
-
# ...but it looks much better as:
-
#
-
# {:a => 5, :b => 2}
-
#
-
# This is idempotent and safe to run on a string multiple times.
-
1
def improve_hash_formatting(inspect_string)
-
inspect_string.gsub(/(\S)=>(\S)/, '\1 => \2')
-
end
-
1
module_function :improve_hash_formatting
-
end
-
-
1
include HashFormatting
-
-
# @api private
-
# Provides default implementations of failure messages, based on the `description`.
-
1
module DefaultFailureMessages
-
# @api private
-
# Provides a good generic failure message. Based on `description`.
-
# When subclassing, if you are not satisfied with this failure message
-
# you often only need to override `description`.
-
# @return [String]
-
1
def failure_message
-
"expected #{description_of @actual} to #{description}"
-
end
-
-
# @api private
-
# Provides a good generic negative failure message. Based on `description`.
-
# When subclassing, if you are not satisfied with this failure message
-
# you often only need to override `description`.
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected #{description_of @actual} not to #{description}"
-
end
-
-
# @private
-
1
def self.has_default_failure_messages?(matcher)
-
matcher.method(:failure_message).owner == self &&
-
matcher.method(:failure_message_when_negated).owner == self
-
rescue NameError
-
false
-
end
-
end
-
-
1
include DefaultFailureMessages
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
# Provides the implementation for `change`.
-
# Not intended to be instantiated directly.
-
1
class Change < BaseMatcher
-
# @api public
-
# Specifies the delta of the expected change.
-
1
def by(expected_delta)
-
2
ChangeRelatively.new(@change_details, expected_delta, :by) do |actual_delta|
-
2
values_match?(expected_delta, actual_delta)
-
end
-
end
-
-
# @api public
-
# Specifies a minimum delta of the expected change.
-
1
def by_at_least(minimum)
-
ChangeRelatively.new(@change_details, minimum, :by_at_least) do |actual_delta|
-
actual_delta >= minimum
-
end
-
end
-
-
# @api public
-
# Specifies a maximum delta of the expected change.
-
1
def by_at_most(maximum)
-
ChangeRelatively.new(@change_details, maximum, :by_at_most) do |actual_delta|
-
actual_delta <= maximum
-
end
-
end
-
-
# @api public
-
# Specifies the new value you expect.
-
1
def to(value)
-
ChangeToValue.new(@change_details, value)
-
end
-
-
# @api public
-
# Specifies the original value.
-
1
def from(value)
-
ChangeFromValue.new(@change_details, value)
-
end
-
-
# @private
-
1
def matches?(event_proc)
-
4
@event_proc = event_proc
-
4
return false unless Proc === event_proc
-
4
raise_block_syntax_error if block_given?
-
4
@change_details.perform_change(event_proc)
-
4
@change_details.changed?
-
end
-
-
1
def does_not_match?(event_proc)
-
4
raise_block_syntax_error if block_given?
-
4
!matches?(event_proc) && Proc === event_proc
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected #{@change_details.message} to have changed, " \
-
"but #{positive_failure_reason}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected #{@change_details.message} not to have changed, " \
-
"but #{negative_failure_reason}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
"change #{@change_details.message}"
-
end
-
-
# @private
-
1
def supports_block_expectations?
-
4
true
-
end
-
-
1
private
-
-
1
def initialize(receiver=nil, message=nil, &block)
-
6
@change_details = ChangeDetails.new(receiver, message, &block)
-
end
-
-
1
def raise_block_syntax_error
-
raise SyntaxError, "Block not received by the `change` matcher. " \
-
"Perhaps you want to use `{ ... }` instead of do/end?"
-
end
-
-
1
def positive_failure_reason
-
return "was not given a block" unless Proc === @event_proc
-
"is still #{description_of @change_details.actual_before}"
-
end
-
-
1
def negative_failure_reason
-
return "was not given a block" unless Proc === @event_proc
-
"did change from #{description_of @change_details.actual_before} " \
-
"to #{description_of @change_details.actual_after}"
-
end
-
end
-
-
# Used to specify a relative change.
-
# @api private
-
1
class ChangeRelatively < BaseMatcher
-
1
def initialize(change_details, expected_delta, relativity, &comparer)
-
2
@change_details = change_details
-
2
@expected_delta = expected_delta
-
2
@relativity = relativity
-
2
@comparer = comparer
-
end
-
-
# @private
-
1
def failure_message
-
"expected #{@change_details.message} to have changed " \
-
"#{@relativity.to_s.tr('_', ' ')} " \
-
"#{description_of @expected_delta}, but #{failure_reason}"
-
end
-
-
# @private
-
1
def matches?(event_proc)
-
2
@event_proc = event_proc
-
2
return false unless Proc === event_proc
-
2
@change_details.perform_change(event_proc)
-
2
@comparer.call(@change_details.actual_delta)
-
end
-
-
# @private
-
1
def does_not_match?(_event_proc)
-
raise NotImplementedError, "`expect { }.not_to change " \
-
"{ }.#{@relativity}()` is not supported"
-
end
-
-
# @private
-
1
def description
-
"change #{@change_details.message} " \
-
"#{@relativity.to_s.tr('_', ' ')} #{description_of @expected_delta}"
-
end
-
-
# @private
-
1
def supports_block_expectations?
-
2
true
-
end
-
-
1
private
-
-
1
def failure_reason
-
return "was not given a block" unless Proc === @event_proc
-
"was changed by #{description_of @change_details.actual_delta}"
-
end
-
end
-
-
# @api private
-
# Base class for specifying a change from and/or to specific values.
-
1
class SpecificValuesChange < BaseMatcher
-
# @private
-
1
MATCH_ANYTHING = ::Object.ancestors.last
-
-
1
def initialize(change_details, from, to)
-
@change_details = change_details
-
@expected_before = from
-
@expected_after = to
-
end
-
-
# @private
-
1
def matches?(event_proc)
-
@event_proc = event_proc
-
return false unless Proc === event_proc
-
@change_details.perform_change(event_proc)
-
@change_details.changed? && matches_before? && matches_after?
-
end
-
-
# @private
-
1
def description
-
"change #{@change_details.message} #{change_description}"
-
end
-
-
# @private
-
1
def failure_message
-
return not_given_a_block_failure unless Proc === @event_proc
-
return before_value_failure unless matches_before?
-
return did_not_change_failure unless @change_details.changed?
-
after_value_failure
-
end
-
-
# @private
-
1
def supports_block_expectations?
-
true
-
end
-
-
1
private
-
-
1
def matches_before?
-
values_match?(@expected_before, @change_details.actual_before)
-
end
-
-
1
def matches_after?
-
values_match?(@expected_after, @change_details.actual_after)
-
end
-
-
1
def before_value_failure
-
"expected #{@change_details.message} " \
-
"to have initially been #{description_of @expected_before}, " \
-
"but was #{description_of @change_details.actual_before}"
-
end
-
-
1
def after_value_failure
-
"expected #{@change_details.message} " \
-
"to have changed to #{description_of @expected_after}, " \
-
"but is now #{description_of @change_details.actual_after}"
-
end
-
-
1
def did_not_change_failure
-
"expected #{@change_details.message} " \
-
"to have changed #{change_description}, but did not change"
-
end
-
-
1
def did_change_failure
-
"expected #{@change_details.message} not to have changed, but " \
-
"did change from #{description_of @change_details.actual_before} " \
-
"to #{description_of @change_details.actual_after}"
-
end
-
-
1
def not_given_a_block_failure
-
"expected #{@change_details.message} to have changed " \
-
"#{change_description}, but was not given a block"
-
end
-
end
-
-
# @api private
-
# Used to specify a change from a specific value
-
# (and, optionally, to a specific value).
-
1
class ChangeFromValue < SpecificValuesChange
-
1
def initialize(change_details, expected_before)
-
@description_suffix = nil
-
super(change_details, expected_before, MATCH_ANYTHING)
-
end
-
-
# @api public
-
# Specifies the new value you expect.
-
1
def to(value)
-
@expected_after = value
-
@description_suffix = " to #{description_of value}"
-
self
-
end
-
-
# @private
-
1
def does_not_match?(event_proc)
-
if @description_suffix
-
raise NotImplementedError, "`expect { }.not_to change { }.to()` " \
-
"is not supported"
-
end
-
-
@event_proc = event_proc
-
return false unless Proc === event_proc
-
@change_details.perform_change(event_proc)
-
!@change_details.changed? && matches_before?
-
end
-
-
# @private
-
1
def failure_message_when_negated
-
return not_given_a_block_failure unless Proc === @event_proc
-
return before_value_failure unless matches_before?
-
did_change_failure
-
end
-
-
1
private
-
-
1
def change_description
-
"from #{description_of @expected_before}#{@description_suffix}"
-
end
-
end
-
-
# @api private
-
# Used to specify a change to a specific value
-
# (and, optionally, from a specific value).
-
1
class ChangeToValue < SpecificValuesChange
-
1
def initialize(change_details, expected_after)
-
@description_suffix = nil
-
super(change_details, MATCH_ANYTHING, expected_after)
-
end
-
-
# @api public
-
# Specifies the original value.
-
1
def from(value)
-
@expected_before = value
-
@description_suffix = " from #{description_of value}"
-
self
-
end
-
-
# @private
-
1
def does_not_match?(_event_proc)
-
raise NotImplementedError, "`expect { }.not_to change { }.to()` " \
-
"is not supported"
-
end
-
-
1
private
-
-
1
def change_description
-
"to #{description_of @expected_after}#{@description_suffix}"
-
end
-
end
-
-
# @private
-
# Encapsulates the details of the before/after values.
-
1
class ChangeDetails
-
1
attr_reader :message, :actual_before, :actual_after
-
-
1
def initialize(receiver=nil, message=nil, &block)
-
6
if receiver && !message
-
raise(
-
ArgumentError,
-
"`change` requires either an object and message " \
-
"(`change(obj, :msg)`) or a block (`change { }`). " \
-
"You passed an object but no message."
-
)
-
end
-
6
@message = message ? "##{message}" : "result"
-
18
@value_proc = block || lambda { receiver.__send__(message) }
-
end
-
-
1
def perform_change(event_proc)
-
6
@actual_before = evaluate_value_proc
-
6
event_proc.call
-
6
@actual_after = evaluate_value_proc
-
end
-
-
1
def changed?
-
4
@actual_before != @actual_after
-
end
-
-
1
def actual_delta
-
2
@actual_after - @actual_before
-
end
-
-
1
private
-
-
1
def evaluate_value_proc
-
12
case val = @value_proc.call
-
when IO # enumerable, but we don't want to dup it.
-
val
-
when Enumerable, String
-
val.dup
-
else
-
12
val
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
# Provides the implementation for `eq`.
-
# Not intended to be instantiated directly.
-
1
class Eq < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"\nexpected: #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"\nexpected: value != #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
"eq #{expected_formatted}"
-
end
-
-
# @api private
-
# @return [Boolean]
-
1
def diffable?
-
true
-
end
-
-
1
private
-
-
1
def match(expected, actual)
-
6
actual == expected
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support "fuzzy_matcher"
-
-
1
module RSpec
-
1
module Matchers
-
# Mixin designed to support the composable matcher features
-
# of RSpec 3+. Mix it into your custom matcher classes to
-
# allow them to be used in a composable fashion.
-
#
-
# @api public
-
1
module Composable
-
# Creates a compound `and` expectation. The matcher will
-
# only pass if both sub-matchers pass.
-
# This can be chained together to form an arbitrarily long
-
# chain of matchers.
-
#
-
# @example
-
# expect(alphabet).to start_with("a").and end_with("z")
-
# expect(alphabet).to start_with("a") & end_with("z")
-
#
-
# @note The negative form (`expect(...).not_to matcher.and other`)
-
# is not supported at this time.
-
1
def and(matcher)
-
BuiltIn::Compound::And.new self, matcher
-
end
-
1
alias & and
-
-
# Creates a compound `or` expectation. The matcher will
-
# pass if either sub-matcher passes.
-
# This can be chained together to form an arbitrarily long
-
# chain of matchers.
-
#
-
# @example
-
# expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
-
# expect(stoplight.color).to eq("red") | eq("green") | eq("yellow")
-
#
-
# @note The negative form (`expect(...).not_to matcher.or other`)
-
# is not supported at this time.
-
1
def or(matcher)
-
BuiltIn::Compound::Or.new self, matcher
-
end
-
1
alias | or
-
-
# Delegates to `#matches?`. Allows matchers to be used in composable
-
# fashion and also supports using matchers in case statements.
-
1
def ===(value)
-
matches?(value)
-
end
-
-
1
private
-
-
# This provides a generic way to fuzzy-match an expected value against
-
# an actual value. It understands nested data structures (e.g. hashes
-
# and arrays) and is able to match against a matcher being used as
-
# the expected value or within the expected value at any level of
-
# nesting.
-
#
-
# Within a custom matcher you are encouraged to use this whenever your
-
# matcher needs to match two values, unless it needs more precise semantics.
-
# For example, the `eq` matcher _does not_ use this as it is meant to
-
# use `==` (and only `==`) for matching.
-
#
-
# @param expected [Object] what is expected
-
# @param actual [Object] the actual value
-
#
-
# @!visibility public
-
1
def values_match?(expected, actual)
-
2
expected = with_matchers_cloned(expected)
-
2
Support::FuzzyMatcher.values_match?(expected, actual)
-
end
-
-
# Returns the description of the given object in a way that is
-
# aware of composed matchers. If the object is a matcher with
-
# a `description` method, returns the description; otherwise
-
# returns `object.inspect`.
-
#
-
# You are encouraged to use this in your custom matcher's
-
# `description`, `failure_message` or
-
# `failure_message_when_negated` implementation if you are
-
# supporting matcher arguments.
-
#
-
# @!visibility public
-
1
def description_of(object)
-
RSpec::Support::ObjectFormatter.format(object)
-
end
-
-
# Transforms the given data structue (typically a hash or array)
-
# into a new data structure that, when `#inspect` is called on it,
-
# will provide descriptions of any contained matchers rather than
-
# the normal `#inspect` output.
-
#
-
# You are encouraged to use this in your custom matcher's
-
# `description`, `failure_message` or
-
# `failure_message_when_negated` implementation if you are
-
# supporting any arguments which may be a data structure
-
# containing matchers.
-
#
-
# @!visibility public
-
1
def surface_descriptions_in(item)
-
if Matchers.is_a_describable_matcher?(item)
-
DescribableItem.new(item)
-
elsif Hash === item
-
Hash[surface_descriptions_in(item.to_a)]
-
elsif Struct === item || unreadable_io?(item)
-
RSpec::Support::ObjectFormatter.format(item)
-
elsif should_enumerate?(item)
-
item.map { |subitem| surface_descriptions_in(subitem) }
-
else
-
item
-
end
-
end
-
-
# @private
-
# Historically, a single matcher instance was only checked
-
# against a single value. Given that the matcher was only
-
# used once, it's been common to memoize some intermediate
-
# calculation that is derived from the `actual` value in
-
# order to reuse that intermediate result in the failure
-
# message.
-
#
-
# This can cause a problem when using such a matcher as an
-
# argument to another matcher in a composed matcher expression,
-
# since the matcher instance may be checked against multiple
-
# values and produce invalid results due to the memoization.
-
#
-
# To deal with this, we clone any matchers in `expected` via
-
# this method when using `values_match?`, so that any memoization
-
# does not "leak" between checks.
-
1
def with_matchers_cloned(object)
-
2
if Matchers.is_a_matcher?(object)
-
object.clone
-
2
elsif Hash === object
-
Hash[with_matchers_cloned(object.to_a)]
-
2
elsif Struct === object || unreadable_io?(object)
-
object
-
2
elsif should_enumerate?(object)
-
object.map { |subobject| with_matchers_cloned(subobject) }
-
else
-
2
object
-
end
-
end
-
-
1
if String.ancestors.include?(Enumerable) # 1.8.7
-
# :nocov:
-
skipped
# Strings are not enumerable on 1.9, and on 1.8 they are an infinitely
-
skipped
# nested enumerable: since ruby lacks a character class, it yields
-
skipped
# 1-character strings, which are themselves enumerable, composed of a
-
skipped
# a single 1-character string, which is an enumerable, etc.
-
skipped
#
-
skipped
# @api private
-
skipped
def should_enumerate?(item)
-
skipped
return false if String === item
-
skipped
Enumerable === item && !(Range === item) && item.none? { |subitem| subitem.equal?(item) }
-
skipped
end
-
# :nocov:
-
else
-
# @api private
-
1
def should_enumerate?(item)
-
2
Enumerable === item && !(Range === item) && item.none? { |subitem| subitem.equal?(item) }
-
end
-
end
-
-
# @api private
-
1
def unreadable_io?(object)
-
2
return false unless IO === object
-
object.each {} # STDOUT is enumerable but raises an error
-
false
-
rescue IOError
-
true
-
end
-
1
module_function :surface_descriptions_in, :should_enumerate?, :unreadable_io?
-
-
# Wraps an item in order to surface its `description` via `inspect`.
-
# @api private
-
1
DescribableItem = Struct.new(:item) do
-
1
def inspect
-
"(#{item.description})"
-
end
-
-
1
def pretty_print(pp)
-
pp.text "(#{item.description})"
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
# Defines the custom matcher DSL.
-
1
module DSL
-
# Defines a custom matcher.
-
# @see RSpec::Matchers
-
1
def define(name, &declarations)
-
warn_about_block_args(name, declarations)
-
define_method name do |*expected, &block_arg|
-
RSpec::Matchers::DSL::Matcher.new(name, declarations, self, *expected, &block_arg)
-
end
-
end
-
1
alias_method :matcher, :define
-
-
1
private
-
-
1
if Proc.method_defined?(:parameters)
-
1
def warn_about_block_args(name, declarations)
-
declarations.parameters.each do |type, arg_name|
-
next unless type == :block
-
RSpec.warning("Your `#{name}` custom matcher receives a block argument (`#{arg_name}`), " \
-
"but due to limitations in ruby, RSpec cannot provide the block. Instead, " \
-
"use the `block_arg` method to access the block")
-
end
-
end
-
else
-
# :nocov:
-
skipped
def warn_about_block_args(*)
-
skipped
# There's no way to detect block params on 1.8 since the method reflection APIs don't expose it
-
skipped
end
-
# :nocov:
-
end
-
-
2
RSpec.configure { |c| c.extend self } if RSpec.respond_to?(:configure)
-
-
# Contains the methods that are available from within the
-
# `RSpec::Matchers.define` DSL for creating custom matchers.
-
1
module Macros
-
# Stores the block that is used to determine whether this matcher passes
-
# or fails. The block should return a boolean value. When the matcher is
-
# passed to `expect(...).to` and the block returns `true`, then the expectation
-
# passes. Similarly, when the matcher is passed to `expect(...).not_to` and the
-
# block returns `false`, then the expectation passes.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :be_even do
-
# match do |actual|
-
# actual.even?
-
# end
-
# end
-
#
-
# expect(4).to be_even # passes
-
# expect(3).not_to be_even # passes
-
# expect(3).to be_even # fails
-
# expect(4).not_to be_even # fails
-
#
-
# By default the match block will swallow expectation errors (e.g.
-
# caused by using an expectation such as `expect(1).to eq 2`), if you
-
# with to allow these to bubble up, pass in the option
-
# `:notify_expectation_failures => true`.
-
#
-
# @param [Hash] options for defining the behavior of the match block.
-
# @yield [Object] actual the actual value (i.e. the value wrapped by `expect`)
-
1
def match(options={}, &match_block)
-
define_user_override(:matches?, match_block) do |actual|
-
@actual = actual
-
RSpec::Support.with_failure_notifier(RAISE_NOTIFIER) do
-
begin
-
super(*actual_arg_for(match_block))
-
rescue RSpec::Expectations::ExpectationNotMetError
-
raise if options[:notify_expectation_failures]
-
false
-
end
-
end
-
end
-
end
-
-
# @private
-
1
RAISE_NOTIFIER = Proc.new { |err, _opts| raise err }
-
-
# Use this to define the block for a negative expectation (`expect(...).not_to`)
-
# when the positive and negative forms require different handling. This
-
# is rarely necessary, but can be helpful, for example, when specifying
-
# asynchronous processes that require different timeouts.
-
#
-
# @yield [Object] actual the actual value (i.e. the value wrapped by `expect`)
-
1
def match_when_negated(&match_block)
-
define_user_override(:does_not_match?, match_block) do |actual|
-
begin
-
@actual = actual
-
RSpec::Support.with_failure_notifier(RAISE_NOTIFIER) do
-
super(*actual_arg_for(match_block))
-
end
-
rescue RSpec::Expectations::ExpectationNotMetError
-
false
-
end
-
end
-
end
-
-
# Use this instead of `match` when the block will raise an exception
-
# rather than returning false to indicate a failure.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :accept_as_valid do |candidate_address|
-
# match_unless_raises ValidationException do |validator|
-
# validator.validate(candidate_address)
-
# end
-
# end
-
#
-
# expect(email_validator).to accept_as_valid("person@company.com")
-
#
-
# @yield [Object] actual the actual object (i.e. the value wrapped by `expect`)
-
1
def match_unless_raises(expected_exception=Exception, &match_block)
-
define_user_override(:matches?, match_block) do |actual|
-
@actual = actual
-
begin
-
super(*actual_arg_for(match_block))
-
rescue expected_exception => @rescued_exception
-
false
-
else
-
true
-
end
-
end
-
end
-
-
# Customizes the failure messsage to use when this matcher is
-
# asked to positively match. Only use this when the message
-
# generated by default doesn't suit your needs.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :have_strength do |expected|
-
# match { your_match_logic }
-
#
-
# failure_message do |actual|
-
# "Expected strength of #{expected}, but had #{actual.strength}"
-
# end
-
# end
-
#
-
# @yield [Object] actual the actual object (i.e. the value wrapped by `expect`)
-
1
def failure_message(&definition)
-
define_user_override(__method__, definition)
-
end
-
-
# Customize the failure messsage to use when this matcher is asked
-
# to negatively match. Only use this when the message generated by
-
# default doesn't suit your needs.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :have_strength do |expected|
-
# match { your_match_logic }
-
#
-
# failure_message_when_negated do |actual|
-
# "Expected not to have strength of #{expected}, but did"
-
# end
-
# end
-
#
-
# @yield [Object] actual the actual object (i.e. the value wrapped by `expect`)
-
1
def failure_message_when_negated(&definition)
-
define_user_override(__method__, definition)
-
end
-
-
# Customize the description to use for one-liners. Only use this when
-
# the description generated by default doesn't suit your needs.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :qualify_for do |expected|
-
# match { your_match_logic }
-
#
-
# description do
-
# "qualify for #{expected}"
-
# end
-
# end
-
#
-
# @yield [Object] actual the actual object (i.e. the value wrapped by `expect`)
-
1
def description(&definition)
-
define_user_override(__method__, definition)
-
end
-
-
# Tells the matcher to diff the actual and expected values in the failure
-
# message.
-
1
def diffable
-
define_method(:diffable?) { true }
-
end
-
-
# Declares that the matcher can be used in a block expectation.
-
# Users will not be able to use your matcher in a block
-
# expectation without declaring this.
-
# (e.g. `expect { do_something }.to matcher`).
-
1
def supports_block_expectations
-
define_method(:supports_block_expectations?) { true }
-
end
-
-
# Convenience for defining methods on this matcher to create a fluent
-
# interface. The trick about fluent interfaces is that each method must
-
# return self in order to chain methods together. `chain` handles that
-
# for you. If the method is invoked and the
-
# `include_chain_clauses_in_custom_matcher_descriptions` config option
-
# hash been enabled, the chained method name and args will be added to the
-
# default description and failure message.
-
#
-
# In the common case where you just want the chained method to store some
-
# value(s) for later use (e.g. in `match`), you can provide one or more
-
# attribute names instead of a block; the chained method will store its
-
# arguments in instance variables with those names, and the values will
-
# be exposed via getters.
-
#
-
# @example
-
#
-
# RSpec::Matchers.define :have_errors_on do |key|
-
# chain :with do |message|
-
# @message = message
-
# end
-
#
-
# match do |actual|
-
# actual.errors[key] == @message
-
# end
-
# end
-
#
-
# expect(minor).to have_errors_on(:age).with("Not old enough to participate")
-
1
def chain(method_name, *attr_names, &definition)
-
unless block_given? ^ attr_names.any?
-
raise ArgumentError, "You must pass either a block or some attribute names (but not both) to `chain`."
-
end
-
-
definition = assign_attributes(attr_names) if attr_names.any?
-
-
define_user_override(method_name, definition) do |*args, &block|
-
super(*args, &block)
-
@chained_method_clauses.push([method_name, args])
-
self
-
end
-
end
-
-
1
def assign_attributes(attr_names)
-
attr_reader(*attr_names)
-
private(*attr_names)
-
-
lambda do |*attr_values|
-
attr_names.zip(attr_values) do |attr_name, attr_value|
-
instance_variable_set(:"@#{attr_name}", attr_value)
-
end
-
end
-
end
-
-
# assign_attributes isn't defined in the private section below because
-
# that makes MRI 1.9.2 emit a warning about private attributes.
-
1
private :assign_attributes
-
-
1
private
-
-
# Does the following:
-
#
-
# - Defines the named method using a user-provided block
-
# in @user_method_defs, which is included as an ancestor
-
# in the singleton class in which we eval the `define` block.
-
# - Defines an overriden definition for the same method
-
# usign the provided `our_def` block.
-
# - Provides a default `our_def` block for the common case
-
# of needing to call the user's definition with `@actual`
-
# as an arg, but only if their block's arity can handle it.
-
#
-
# This compiles the user block into an actual method, allowing
-
# them to use normal method constructs like `return`
-
# (e.g. for a early guard statement), while allowing us to define
-
# an override that can provide the wrapped handling
-
# (e.g. assigning `@actual`, rescueing errors, etc) and
-
# can `super` to the user's definition.
-
1
def define_user_override(method_name, user_def, &our_def)
-
@user_method_defs.__send__(:define_method, method_name, &user_def)
-
our_def ||= lambda { super(*actual_arg_for(user_def)) }
-
define_method(method_name, &our_def)
-
end
-
-
# Defines deprecated macro methods from RSpec 2 for backwards compatibility.
-
# @deprecated Use the methods from {Macros} instead.
-
1
module Deprecated
-
# @deprecated Use {Macros#match} instead.
-
1
def match_for_should(&definition)
-
RSpec.deprecate("`match_for_should`", :replacement => "`match`")
-
match(&definition)
-
end
-
-
# @deprecated Use {Macros#match_when_negated} instead.
-
1
def match_for_should_not(&definition)
-
RSpec.deprecate("`match_for_should_not`", :replacement => "`match_when_negated`")
-
match_when_negated(&definition)
-
end
-
-
# @deprecated Use {Macros#failure_message} instead.
-
1
def failure_message_for_should(&definition)
-
RSpec.deprecate("`failure_message_for_should`", :replacement => "`failure_message`")
-
failure_message(&definition)
-
end
-
-
# @deprecated Use {Macros#failure_message_when_negated} instead.
-
1
def failure_message_for_should_not(&definition)
-
RSpec.deprecate("`failure_message_for_should_not`", :replacement => "`failure_message_when_negated`")
-
failure_message_when_negated(&definition)
-
end
-
end
-
end
-
-
# Defines default implementations of the matcher
-
# protocol methods for custom matchers. You can
-
# override any of these using the {RSpec::Matchers::DSL::Macros Macros} methods
-
# from within an `RSpec::Matchers.define` block.
-
1
module DefaultImplementations
-
1
include BuiltIn::BaseMatcher::DefaultFailureMessages
-
-
# @api private
-
# Used internally by objects returns by `should` and `should_not`.
-
1
def diffable?
-
false
-
end
-
-
# The default description.
-
1
def description
-
english_name = EnglishPhrasing.split_words(name)
-
expected_list = EnglishPhrasing.list(expected)
-
"#{english_name}#{expected_list}#{chained_method_clause_sentences}"
-
end
-
-
# Matchers do not support block expectations by default. You
-
# must opt-in.
-
1
def supports_block_expectations?
-
false
-
end
-
-
# Most matchers do not expect call stack jumps.
-
1
def expects_call_stack_jump?
-
false
-
end
-
-
1
private
-
-
1
def chained_method_clause_sentences
-
return '' unless Expectations.configuration.include_chain_clauses_in_custom_matcher_descriptions?
-
-
@chained_method_clauses.map do |(method_name, method_args)|
-
english_name = EnglishPhrasing.split_words(method_name)
-
arg_list = EnglishPhrasing.list(method_args)
-
" #{english_name}#{arg_list}"
-
end.join
-
end
-
end
-
-
# The class used for custom matchers. The block passed to
-
# `RSpec::Matchers.define` will be evaluated in the context
-
# of the singleton class of an instance, and will have the
-
# {RSpec::Matchers::DSL::Macros Macros} methods available.
-
1
class Matcher
-
# Provides default implementations for the matcher protocol methods.
-
1
include DefaultImplementations
-
-
# Allows expectation expressions to be used in the match block.
-
1
include RSpec::Matchers
-
-
# Supports the matcher composability features of RSpec 3+.
-
1
include Composable
-
-
# Makes the macro methods available to an `RSpec::Matchers.define` block.
-
1
extend Macros
-
1
extend Macros::Deprecated
-
-
# Exposes the value being matched against -- generally the object
-
# object wrapped by `expect`.
-
1
attr_reader :actual
-
-
# Exposes the exception raised during the matching by `match_unless_raises`.
-
# Could be useful to extract details for a failure message.
-
1
attr_reader :rescued_exception
-
-
# The block parameter used in the expectation
-
1
attr_reader :block_arg
-
-
# The name of the matcher.
-
1
attr_reader :name
-
-
# @api private
-
1
def initialize(name, declarations, matcher_execution_context, *expected, &block_arg)
-
@name = name
-
@actual = nil
-
@expected_as_array = expected
-
@matcher_execution_context = matcher_execution_context
-
@chained_method_clauses = []
-
@block_arg = block_arg
-
-
class << self
-
# See `Macros#define_user_override` above, for an explanation.
-
include(@user_method_defs = Module.new)
-
self
-
end.class_exec(*expected, &declarations)
-
end
-
-
# Provides the expected value. This will return an array if
-
# multiple arguments were passed to the matcher; otherwise it
-
# will return a single value.
-
# @see #expected_as_array
-
1
def expected
-
if expected_as_array.size == 1
-
expected_as_array[0]
-
else
-
expected_as_array
-
end
-
end
-
-
# Returns the expected value as an an array. This exists primarily
-
# to aid in upgrading from RSpec 2.x, since in RSpec 2, `expected`
-
# always returned an array.
-
# @see #expected
-
1
attr_reader :expected_as_array
-
-
# Adds the name (rather than a cryptic hex number)
-
# so we can identify an instance of
-
# the matcher in error messages (e.g. for `NoMethodError`)
-
1
def inspect
-
"#<#{self.class.name} #{name}>"
-
end
-
-
1
if RUBY_VERSION.to_f >= 1.9
-
# Indicates that this matcher responds to messages
-
# from the `@matcher_execution_context` as well.
-
# Also, supports getting a method object for such methods.
-
1
def respond_to_missing?(method, include_private=false)
-
super || @matcher_execution_context.respond_to?(method, include_private)
-
end
-
else # for 1.8.7
-
# :nocov:
-
skipped
# Indicates that this matcher responds to messages
-
skipped
# from the `@matcher_execution_context` as well.
-
skipped
def respond_to?(method, include_private=false)
-
skipped
super || @matcher_execution_context.respond_to?(method, include_private)
-
skipped
end
-
# :nocov:
-
end
-
-
1
private
-
-
1
def actual_arg_for(block)
-
block.arity.zero? ? [] : [@actual]
-
end
-
-
# Takes care of forwarding unhandled messages to the
-
# `@matcher_execution_context` (typically the current
-
# running `RSpec::Core::Example`). This is needed by
-
# rspec-rails so that it can define matchers that wrap
-
# Rails' test helper methods, but it's also a useful
-
# feature in its own right.
-
1
def method_missing(method, *args, &block)
-
if @matcher_execution_context.respond_to?(method)
-
@matcher_execution_context.__send__ method, *args, &block
-
else
-
super(method, *args, &block)
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
RSpec::Matchers.extend RSpec::Matchers::DSL
-
1
module RSpec
-
1
module Matchers
-
# Facilitates converting ruby objects to English phrases.
-
1
module EnglishPhrasing
-
# Converts a symbol into an English expression.
-
#
-
# split_words(:banana_creme_pie) #=> "banana creme pie"
-
#
-
1
def self.split_words(sym)
-
sym.to_s.gsub(/_/, ' ')
-
end
-
-
# @note The returned string has a leading space except
-
# when given an empty list.
-
#
-
# Converts an object (often a collection of objects)
-
# into an English list.
-
#
-
# list(['banana', 'kiwi', 'mango'])
-
# #=> " \"banana\", \"kiwi\", and \"mango\""
-
#
-
# Given an empty collection, returns the empty string.
-
#
-
# list([]) #=> ""
-
#
-
1
def self.list(obj)
-
return " #{RSpec::Support::ObjectFormatter.format(obj)}" if !obj || Struct === obj
-
items = Array(obj).map { |w| RSpec::Support::ObjectFormatter.format(w) }
-
case items.length
-
when 0
-
""
-
when 1
-
" #{items[0]}"
-
when 2
-
" #{items[0]} and #{items[1]}"
-
else
-
" #{items[0...-1].join(', ')}, and #{items[-1]}"
-
end
-
end
-
-
1
if RUBY_VERSION == '1.8.7'
-
# Not sure why, but on travis on 1.8.7 we have gotten these warnings:
-
# lib/rspec/matchers/english_phrasing.rb:28: warning: default `to_a' will be obsolete
-
# So it appears that `Array` can trigger that (e.g. by calling `to_a` on the passed object?)
-
# So here we replace `Kernel#Array` with our own warning-free implementation for 1.8.7.
-
# @private
-
# rubocop:disable Style/MethodName
-
def self.Array(obj)
-
case obj
-
when Array then obj
-
else [obj]
-
end
-
end
-
# rubocop:enable Style/MethodName
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
# @api private
-
# Handles list of expected values when there is a need to render
-
# multiple diffs. Also can handle one value.
-
1
class ExpectedsForMultipleDiffs
-
# @private
-
# Default diff label when there is only one matcher in diff
-
# output
-
1
DEFAULT_DIFF_LABEL = "Diff:".freeze
-
-
# @private
-
# Maximum readable matcher description length
-
1
DESCRIPTION_MAX_LENGTH = 65
-
-
1
def initialize(expected_list)
-
@expected_list = expected_list
-
end
-
-
# @api private
-
# Wraps provided expected value in instance of
-
# ExpectedForMultipleDiffs. If provided value is already an
-
# ExpectedForMultipleDiffs then it just returns it.
-
# @param [Any] expected value to be wrapped
-
# @return [RSpec::Matchers::ExpectedsForMultipleDiffs]
-
1
def self.from(expected)
-
return expected if self === expected
-
new([[expected, DEFAULT_DIFF_LABEL]])
-
end
-
-
# @api private
-
# Wraps provided matcher list in instance of
-
# ExpectedForMultipleDiffs.
-
# @param [Array<Any>] matchers list of matchers to wrap
-
# @return [RSpec::Matchers::ExpectedsForMultipleDiffs]
-
1
def self.for_many_matchers(matchers)
-
new(matchers.map { |m| [m.expected, diff_label_for(m)] })
-
end
-
-
# @api private
-
# Returns message with diff(s) appended for provided differ
-
# factory and actual value if there are any
-
# @param [String] message original failure message
-
# @param [Proc] differ
-
# @param [Any] actual value
-
# @return [String]
-
1
def message_with_diff(message, differ, actual)
-
diff = diffs(differ, actual)
-
message = "#{message}\n#{diff}" unless diff.empty?
-
message
-
end
-
-
1
private
-
-
1
def self.diff_label_for(matcher)
-
"Diff for (#{truncated(RSpec::Support::ObjectFormatter.format(matcher))}):"
-
end
-
-
1
def self.truncated(description)
-
return description if description.length <= DESCRIPTION_MAX_LENGTH
-
description[0...DESCRIPTION_MAX_LENGTH - 3] << "..."
-
end
-
-
1
def diffs(differ, actual)
-
@expected_list.map do |(expected, diff_label)|
-
diff = differ.diff(actual, expected)
-
next if diff.strip.empty?
-
"#{diff_label}#{diff}"
-
end.compact.join("\n")
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
class << self
-
# @private
-
1
attr_accessor :last_matcher, :last_expectation_handler
-
end
-
-
# @api private
-
# Used by rspec-core to clear the state used to generate
-
# descriptions after an example.
-
1
def self.clear_generated_description
-
14
self.last_matcher = nil
-
14
self.last_expectation_handler = nil
-
end
-
-
# @api private
-
# Generates an an example description based on the last expectation.
-
# Used by rspec-core's one-liner syntax.
-
1
def self.generated_description
-
return nil if last_expectation_handler.nil?
-
"#{last_expectation_handler.verb} #{last_description}"
-
end
-
-
1
private
-
-
1
def self.last_description
-
last_matcher.respond_to?(:description) ? last_matcher.description : <<-MESSAGE
-
When you call a matcher in an example without a String, like this:
-
-
specify { expect(object).to matcher }
-
-
or this:
-
-
it { is_expected.to matcher }
-
-
RSpec expects the matcher to have a #description method. You should either
-
add a String to the example this matcher is being used in, or give it a
-
description method. Then you won't have to suffer this lengthy warning again.
-
MESSAGE
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
# Provides the necessary plumbing to wrap a matcher with a decorator.
-
# @private
-
1
class MatcherDelegator
-
1
include Composable
-
1
attr_reader :base_matcher
-
-
1
def initialize(base_matcher)
-
@base_matcher = base_matcher
-
end
-
-
1
def method_missing(*args, &block)
-
base_matcher.__send__(*args, &block)
-
end
-
-
1
if ::RUBY_VERSION.to_f > 1.8
-
1
def respond_to_missing?(name, include_all=false)
-
super || base_matcher.respond_to?(name, include_all)
-
end
-
else
-
# :nocov:
-
skipped
def respond_to?(name, include_all=false)
-
skipped
super || base_matcher.respond_to?(name, include_all)
-
skipped
end
-
# :nocov:
-
end
-
-
1
def initialize_copy(other)
-
@base_matcher = @base_matcher.clone
-
super
-
end
-
end
-
end
-
end
-
1
require 'rspec/support'
-
1
RSpec::Support.require_rspec_support 'caller_filter'
-
1
RSpec::Support.require_rspec_support 'warnings'
-
1
RSpec::Support.require_rspec_support 'ruby_features'
-
-
22
RSpec::Support.define_optimized_require_for_rspec(:mocks) { |f| require_relative f }
-
-
%w[
-
instance_method_stasher
-
method_double
-
argument_matchers
-
example_methods
-
proxy
-
test_double
-
argument_list_matcher
-
message_expectation
-
order_group
-
error_generator
-
space
-
mutate_const
-
targets
-
syntax
-
configuration
-
verifying_double
-
version
-
18
].each { |name| RSpec::Support.require_rspec_mocks name }
-
-
# Share the top-level RSpec namespace, because we are a core supported
-
# extension.
-
1
module RSpec
-
# Contains top-level utility methods. While this contains a few
-
# public methods, these are not generally meant to be called from
-
# a test or example. They exist primarily for integration with
-
# test frameworks (such as rspec-core).
-
1
module Mocks
-
# Performs per-test/example setup. This should be called before
-
# an test or example begins.
-
1
def self.setup
-
14
@space_stack << (@space = space.new_scope)
-
end
-
-
# Verifies any message expectations that were set during the
-
# test or example. This should be called at the end of an example.
-
1
def self.verify
-
14
space.verify_all
-
end
-
-
# Cleans up all test double state (including any methods that were
-
# redefined on partial doubles). This _must_ be called after
-
# each example, even if an error was raised during the example.
-
1
def self.teardown
-
14
space.reset_all
-
14
@space_stack.pop
-
14
@space = @space_stack.last || @root_space
-
end
-
-
# Adds an allowance (stub) on `subject`
-
#
-
# @param subject the subject to which the message will be added
-
# @param message a symbol, representing the message that will be
-
# added.
-
# @param opts a hash of options, :expected_from is used to set the
-
# original call site
-
# @yield an optional implementation for the allowance
-
#
-
# @example Defines the implementation of `foo` on `bar`, using the passed block
-
# x = 0
-
# RSpec::Mocks.allow_message(bar, :foo) { x += 1 }
-
1
def self.allow_message(subject, message, opts={}, &block)
-
space.proxy_for(subject).add_stub(message, opts, &block)
-
end
-
-
# Sets a message expectation on `subject`.
-
# @param subject the subject on which the message will be expected
-
# @param message a symbol, representing the message that will be
-
# expected.
-
# @param opts a hash of options, :expected_from is used to set the
-
# original call site
-
# @yield an optional implementation for the expectation
-
#
-
# @example Expect the message `foo` to receive `bar`, then call it
-
# RSpec::Mocks.expect_message(bar, :foo)
-
# bar.foo
-
1
def self.expect_message(subject, message, opts={}, &block)
-
space.proxy_for(subject).add_message_expectation(message, opts, &block)
-
end
-
-
# Call the passed block and verify mocks after it has executed. This allows
-
# mock usage in arbitrary places, such as a `before(:all)` hook.
-
1
def self.with_temporary_scope
-
setup
-
-
begin
-
yield
-
verify
-
ensure
-
teardown
-
end
-
end
-
-
1
class << self
-
# @private
-
1
attr_reader :space
-
end
-
1
@space_stack = []
-
1
@root_space = @space = RSpec::Mocks::RootSpace.new
-
-
# @private
-
1
IGNORED_BACKTRACE_LINE = 'this backtrace line is ignored'
-
-
# To speed up boot time a bit, delay loading optional or rarely
-
# used features until their first use.
-
1
autoload :AnyInstance, "rspec/mocks/any_instance"
-
1
autoload :ExpectChain, "rspec/mocks/message_chain"
-
1
autoload :StubChain, "rspec/mocks/message_chain"
-
1
autoload :MarshalExtension, "rspec/mocks/marshal_extension"
-
-
# Namespace for mock-related matchers.
-
1
module Matchers
-
# @private
-
# just a "tag" for rspec-mock matchers detection
-
1
module Matcher; end
-
-
1
autoload :HaveReceived, "rspec/mocks/matchers/have_received"
-
1
autoload :Receive, "rspec/mocks/matchers/receive"
-
1
autoload :ReceiveMessageChain, "rspec/mocks/matchers/receive_message_chain"
-
1
autoload :ReceiveMessages, "rspec/mocks/matchers/receive_messages"
-
end
-
end
-
end
-
# We intentionally do not use the `RSpec::Support.require...` methods
-
# here so that this file can be loaded individually, as documented
-
# below.
-
1
require 'rspec/mocks/argument_matchers'
-
1
require 'rspec/support/fuzzy_matcher'
-
-
1
module RSpec
-
1
module Mocks
-
# Wrapper for matching arguments against a list of expected values. Used by
-
# the `with` method on a `MessageExpectation`:
-
#
-
# expect(object).to receive(:message).with(:a, 'b', 3)
-
# object.message(:a, 'b', 3)
-
#
-
# Values passed to `with` can be literal values or argument matchers that
-
# match against the real objects .e.g.
-
#
-
# expect(object).to receive(:message).with(hash_including(:a => 'b'))
-
#
-
# Can also be used directly to match the contents of any `Array`. This
-
# enables 3rd party mocking libs to take advantage of rspec's argument
-
# matching without using the rest of rspec-mocks.
-
#
-
# require 'rspec/mocks/argument_list_matcher'
-
# include RSpec::Mocks::ArgumentMatchers
-
#
-
# arg_list_matcher = RSpec::Mocks::ArgumentListMatcher.new(123, hash_including(:a => 'b'))
-
# arg_list_matcher.args_match?(123, :a => 'b')
-
#
-
# This class is immutable.
-
#
-
# @see ArgumentMatchers
-
1
class ArgumentListMatcher
-
# @private
-
1
attr_reader :expected_args
-
-
# @api public
-
# @param [Array] expected_args a list of expected literals and/or argument matchers
-
#
-
# Initializes an `ArgumentListMatcher` with a collection of literal
-
# values and/or argument matchers.
-
#
-
# @see ArgumentMatchers
-
# @see #args_match?
-
1
def initialize(*expected_args)
-
1
@expected_args = expected_args
-
1
ensure_expected_args_valid!
-
end
-
-
# @api public
-
# @param [Array] args
-
#
-
# Matches each element in the `expected_args` against the element in the same
-
# position of the arguments passed to `new`.
-
#
-
# @see #initialize
-
1
def args_match?(*args)
-
Support::FuzzyMatcher.values_match?(resolve_expected_args_based_on(args), args)
-
end
-
-
# @private
-
# Resolves abstract arg placeholders like `no_args` and `any_args` into
-
# a more concrete arg list based on the provided `actual_args`.
-
1
def resolve_expected_args_based_on(actual_args)
-
return [] if [ArgumentMatchers::NoArgsMatcher::INSTANCE] == expected_args
-
-
any_args_index = expected_args.index { |a| ArgumentMatchers::AnyArgsMatcher::INSTANCE == a }
-
return expected_args unless any_args_index
-
-
replace_any_args_with_splat_of_anything(any_args_index, actual_args.count)
-
end
-
-
1
private
-
-
1
def replace_any_args_with_splat_of_anything(before_count, actual_args_count)
-
any_args_count = actual_args_count - expected_args.count + 1
-
after_count = expected_args.count - before_count - 1
-
-
any_args = 1.upto(any_args_count).map { ArgumentMatchers::AnyArgMatcher::INSTANCE }
-
expected_args.first(before_count) + any_args + expected_args.last(after_count)
-
end
-
-
1
def ensure_expected_args_valid!
-
2
if expected_args.count { |a| ArgumentMatchers::AnyArgsMatcher::INSTANCE == a } > 1
-
raise ArgumentError, "`any_args` can only be passed to " \
-
"`with` once but you have passed it multiple times."
-
1
elsif expected_args.count > 1 && expected_args.any? { |a| ArgumentMatchers::NoArgsMatcher::INSTANCE == a }
-
raise ArgumentError, "`no_args` can only be passed as a " \
-
"singleton argument to `with` (i.e. `with(no_args)`), " \
-
"but you have passed additional arguments."
-
end
-
end
-
-
# Value that will match all argument lists.
-
#
-
# @private
-
1
MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher::INSTANCE)
-
end
-
end
-
end
-
# This cannot take advantage of our relative requires, since this file is a
-
# dependency of `rspec/mocks/argument_list_matcher.rb`. See comment there for
-
# details.
-
1
require 'rspec/support/matcher_definition'
-
-
1
module RSpec
-
1
module Mocks
-
# ArgumentMatchers are placeholders that you can include in message
-
# expectations to match arguments against a broader check than simple
-
# equality.
-
#
-
# With the exception of `any_args` and `no_args`, they all match against
-
# the arg in same position in the argument list.
-
#
-
# @see ArgumentListMatcher
-
1
module ArgumentMatchers
-
# Acts like an arg splat, matching any number of args at any point in an arg list.
-
#
-
# @example
-
# expect(object).to receive(:message).with(1, 2, any_args)
-
#
-
# # matches any of these:
-
# object.message(1, 2)
-
# object.message(1, 2, 3)
-
# object.message(1, 2, 3, 4)
-
1
def any_args
-
AnyArgsMatcher::INSTANCE
-
end
-
-
# Matches any argument at all.
-
#
-
# @example
-
# expect(object).to receive(:message).with(anything)
-
1
def anything
-
AnyArgMatcher::INSTANCE
-
end
-
-
# Matches no arguments.
-
#
-
# @example
-
# expect(object).to receive(:message).with(no_args)
-
1
def no_args
-
NoArgsMatcher::INSTANCE
-
end
-
-
# Matches if the actual argument responds to the specified messages.
-
#
-
# @example
-
# expect(object).to receive(:message).with(duck_type(:hello))
-
# expect(object).to receive(:message).with(duck_type(:hello, :goodbye))
-
1
def duck_type(*args)
-
DuckTypeMatcher.new(*args)
-
end
-
-
# Matches a boolean value.
-
#
-
# @example
-
# expect(object).to receive(:message).with(boolean())
-
1
def boolean
-
BooleanMatcher::INSTANCE
-
end
-
-
# Matches a hash that includes the specified key(s) or key/value pairs.
-
# Ignores any additional keys.
-
#
-
# @example
-
# expect(object).to receive(:message).with(hash_including(:key => val))
-
# expect(object).to receive(:message).with(hash_including(:key))
-
# expect(object).to receive(:message).with(hash_including(:key, :key2 => val2))
-
1
def hash_including(*args)
-
HashIncludingMatcher.new(ArgumentMatchers.anythingize_lonely_keys(*args))
-
end
-
-
# Matches an array that includes the specified items at least once.
-
# Ignores duplicates and additional values
-
#
-
# @example
-
# expect(object).to receive(:message).with(array_including(1,2,3))
-
# expect(object).to receive(:message).with(array_including([1,2,3]))
-
1
def array_including(*args)
-
actually_an_array = Array === args.first && args.count == 1 ? args.first : args
-
ArrayIncludingMatcher.new(actually_an_array)
-
end
-
-
# Matches a hash that doesn't include the specified key(s) or key/value.
-
#
-
# @example
-
# expect(object).to receive(:message).with(hash_excluding(:key => val))
-
# expect(object).to receive(:message).with(hash_excluding(:key))
-
# expect(object).to receive(:message).with(hash_excluding(:key, :key2 => :val2))
-
1
def hash_excluding(*args)
-
HashExcludingMatcher.new(ArgumentMatchers.anythingize_lonely_keys(*args))
-
end
-
-
1
alias_method :hash_not_including, :hash_excluding
-
-
# Matches if `arg.instance_of?(klass)`
-
#
-
# @example
-
# expect(object).to receive(:message).with(instance_of(Thing))
-
1
def instance_of(klass)
-
InstanceOf.new(klass)
-
end
-
-
1
alias_method :an_instance_of, :instance_of
-
-
# Matches if `arg.kind_of?(klass)`
-
#
-
# @example
-
# expect(object).to receive(:message).with(kind_of(Thing))
-
1
def kind_of(klass)
-
KindOf.new(klass)
-
end
-
-
1
alias_method :a_kind_of, :kind_of
-
-
# @private
-
1
def self.anythingize_lonely_keys(*args)
-
hash = args.last.class == Hash ? args.delete_at(-1) : {}
-
args.each { | arg | hash[arg] = AnyArgMatcher::INSTANCE }
-
hash
-
end
-
-
# Intended to be subclassed by stateless, immutable argument matchers.
-
# Provides a `<klass name>::INSTANCE` constant for accessing a global
-
# singleton instance of the matcher. There is no need to construct
-
# multiple instance since there is no state. It also facilities the
-
# special case logic we need for some of these matchers, by making it
-
# easy to do comparisons like: `[klass::INSTANCE] == args` rather than
-
# `args.count == 1 && klass === args.first`.
-
#
-
# @private
-
1
class SingletonMatcher
-
1
private_class_method :new
-
-
1
def self.inherited(subklass)
-
4
subklass.const_set(:INSTANCE, subklass.send(:new))
-
end
-
end
-
-
# @private
-
1
class AnyArgsMatcher < SingletonMatcher
-
1
def description
-
"*(any args)"
-
end
-
end
-
-
# @private
-
1
class AnyArgMatcher < SingletonMatcher
-
1
def ===(_other)
-
true
-
end
-
-
1
def description
-
"anything"
-
end
-
end
-
-
# @private
-
1
class NoArgsMatcher < SingletonMatcher
-
1
def description
-
"no args"
-
end
-
end
-
-
# @private
-
1
class BooleanMatcher < SingletonMatcher
-
1
def ===(value)
-
true == value || false == value
-
end
-
-
1
def description
-
"boolean"
-
end
-
end
-
-
# @private
-
1
class BaseHashMatcher
-
1
def initialize(expected)
-
@expected = expected
-
end
-
-
1
def ===(predicate, actual)
-
@expected.__send__(predicate) do |k, v|
-
actual.key?(k) && Support::FuzzyMatcher.values_match?(v, actual[k])
-
end
-
rescue NoMethodError
-
false
-
end
-
-
1
def description(name)
-
"#{name}(#{formatted_expected_hash.inspect.sub(/^\{/, "").sub(/\}$/, "")})"
-
end
-
-
1
private
-
-
1
def formatted_expected_hash
-
Hash[
-
@expected.map do |k, v|
-
k = RSpec::Support.rspec_description_for_object(k)
-
v = RSpec::Support.rspec_description_for_object(v)
-
-
[k, v]
-
end
-
]
-
end
-
end
-
-
# @private
-
1
class HashIncludingMatcher < BaseHashMatcher
-
1
def ===(actual)
-
super(:all?, actual)
-
end
-
-
1
def description
-
super("hash_including")
-
end
-
end
-
-
# @private
-
1
class HashExcludingMatcher < BaseHashMatcher
-
1
def ===(actual)
-
super(:none?, actual)
-
end
-
-
1
def description
-
super("hash_not_including")
-
end
-
end
-
-
# @private
-
1
class ArrayIncludingMatcher
-
1
def initialize(expected)
-
@expected = expected
-
end
-
-
1
def ===(actual)
-
actual = actual.uniq
-
@expected.uniq.all? do |expected_element|
-
actual.any? do |actual_element|
-
RSpec::Support::FuzzyMatcher.values_match?(expected_element, actual_element)
-
end
-
end
-
end
-
-
1
def description
-
"array_including(#{formatted_expected_values})"
-
end
-
-
1
private
-
-
1
def formatted_expected_values
-
@expected.map do |x|
-
RSpec::Support.rspec_description_for_object(x)
-
end.join(", ")
-
end
-
end
-
-
# @private
-
1
class DuckTypeMatcher
-
1
def initialize(*methods_to_respond_to)
-
@methods_to_respond_to = methods_to_respond_to
-
end
-
-
1
def ===(value)
-
@methods_to_respond_to.all? { |message| value.respond_to?(message) }
-
end
-
-
1
def description
-
"duck_type(#{@methods_to_respond_to.map(&:inspect).join(', ')})"
-
end
-
end
-
-
# @private
-
1
class InstanceOf
-
1
def initialize(klass)
-
@klass = klass
-
end
-
-
1
def ===(actual)
-
actual.instance_of?(@klass)
-
end
-
-
1
def description
-
"an_instance_of(#{@klass.name})"
-
end
-
end
-
-
# @private
-
1
class KindOf
-
1
def initialize(klass)
-
@klass = klass
-
end
-
-
1
def ===(actual)
-
actual.kind_of?(@klass)
-
end
-
-
1
def description
-
"kind of #{@klass.name}"
-
end
-
end
-
-
1
matcher_namespace = name + '::'
-
1
::RSpec::Support.register_matcher_definition do |object|
-
# This is the best we have for now. We should tag all of our matchers
-
# with a module or something so we can test for it directly.
-
#
-
# (Note Module#parent in ActiveSupport is defined in a similar way.)
-
begin
-
object.class.name.include?(matcher_namespace)
-
rescue NoMethodError
-
# Some objects, like BasicObject, don't implemented standard
-
# reflection methods.
-
false
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# Provides configuration options for rspec-mocks.
-
1
class Configuration
-
1
def initialize
-
1
@allow_message_expectations_on_nil = nil
-
1
@yield_receiver_to_any_instance_implementation_blocks = true
-
1
@verify_doubled_constant_names = false
-
1
@transfer_nested_constants = false
-
1
@verify_partial_doubles = false
-
end
-
-
# Sets whether RSpec will warn, ignore, or fail a test when
-
# expectations are set on nil.
-
# By default, when this flag is not set, warning messages are issued when
-
# expectations are set on nil. This is to prevent false-positives and to
-
# catch potential bugs early on.
-
# When set to `true`, warning messages are suppressed.
-
# When set to `false`, it will raise an error.
-
#
-
# @example
-
# RSpec.configure do |config|
-
# config.mock_with :rspec do |mocks|
-
# mocks.allow_message_expectations_on_nil = false
-
# end
-
# end
-
1
attr_accessor :allow_message_expectations_on_nil
-
-
1
def yield_receiver_to_any_instance_implementation_blocks?
-
@yield_receiver_to_any_instance_implementation_blocks
-
end
-
-
# Sets whether or not RSpec will yield the receiving instance of a
-
# message to blocks that are used for any_instance stub implementations.
-
# When set, the first yielded argument will be the receiving instance.
-
# Defaults to `true`.
-
#
-
# @example
-
# RSpec.configure do |rspec|
-
# rspec.mock_with :rspec do |mocks|
-
# mocks.yield_receiver_to_any_instance_implementation_blocks = false
-
# end
-
# end
-
1
attr_writer :yield_receiver_to_any_instance_implementation_blocks
-
-
# Adds `stub` and `should_receive` to the given
-
# modules or classes. This is usually only necessary
-
# if you application uses some proxy classes that
-
# "strip themselves down" to a bare minimum set of
-
# methods and remove `stub` and `should_receive` in
-
# the process.
-
#
-
# @example
-
# RSpec.configure do |rspec|
-
# rspec.mock_with :rspec do |mocks|
-
# mocks.add_stub_and_should_receive_to Delegator
-
# end
-
# end
-
#
-
1
def add_stub_and_should_receive_to(*modules)
-
modules.each do |mod|
-
Syntax.enable_should(mod)
-
end
-
end
-
-
# Provides the ability to set either `expect`,
-
# `should` or both syntaxes. RSpec uses `expect`
-
# syntax by default. This is needed if you want to
-
# explicitly enable `should` syntax and/or explicitly
-
# disable `expect` syntax.
-
#
-
# @example
-
# RSpec.configure do |rspec|
-
# rspec.mock_with :rspec do |mocks|
-
# mocks.syntax = [:expect, :should]
-
# end
-
# end
-
#
-
1
def syntax=(*values)
-
1
syntaxes = values.flatten
-
1
if syntaxes.include?(:expect)
-
1
Syntax.enable_expect
-
else
-
Syntax.disable_expect
-
end
-
-
1
if syntaxes.include?(:should)
-
1
Syntax.enable_should
-
else
-
Syntax.disable_should
-
end
-
end
-
-
# Returns an array with a list of syntaxes
-
# that are enabled.
-
#
-
# @example
-
# unless RSpec::Mocks.configuration.syntax.include?(:expect)
-
# raise "this RSpec extension gem requires the rspec-mocks `:expect` syntax"
-
# end
-
#
-
1
def syntax
-
syntaxes = []
-
syntaxes << :should if Syntax.should_enabled?
-
syntaxes << :expect if Syntax.expect_enabled?
-
syntaxes
-
end
-
-
1
def verify_doubled_constant_names?
-
!!@verify_doubled_constant_names
-
end
-
-
# When this is set to true, an error will be raised when
-
# `instance_double` or `class_double` is given the name of an undefined
-
# constant. You probably only want to set this when running your entire
-
# test suite, with all production code loaded. Setting this for an
-
# isolated unit test will prevent you from being able to isolate it!
-
1
attr_writer :verify_doubled_constant_names
-
-
# Provides a way to perform customisations when verifying doubles.
-
#
-
# @example
-
# RSpec::Mocks.configuration.before_verifying_doubles do |ref|
-
# ref.some_method!
-
# end
-
1
def before_verifying_doubles(&block)
-
verifying_double_callbacks << block
-
end
-
1
alias :when_declaring_verifying_double :before_verifying_doubles
-
-
# @api private
-
# Returns an array of blocks to call when verifying doubles
-
1
def verifying_double_callbacks
-
@verifying_double_callbacks ||= []
-
end
-
-
1
def transfer_nested_constants?
-
!!@transfer_nested_constants
-
end
-
-
# Sets the default for the `transfer_nested_constants` option when
-
# stubbing constants.
-
1
attr_writer :transfer_nested_constants
-
-
# When set to true, partial mocks will be verified the same as object
-
# doubles. Any stubs will have their arguments checked against the original
-
# method, and methods that do not exist cannot be stubbed.
-
1
def verify_partial_doubles=(val)
-
1
@verify_partial_doubles = !!val
-
end
-
-
1
def verify_partial_doubles?
-
@verify_partial_doubles
-
end
-
-
1
if ::RSpec.respond_to?(:configuration)
-
1
def color?
-
::RSpec.configuration.color_enabled?
-
end
-
else
-
# Indicates whether or not diffs should be colored.
-
# Delegates to rspec-core's color option if rspec-core
-
# is loaded; otherwise you can set it here.
-
attr_writer :color
-
-
# Indicates whether or not diffs should be colored.
-
# Delegates to rspec-core's color option if rspec-core
-
# is loaded; otherwise you can set it here.
-
def color?
-
@color
-
end
-
end
-
-
# Monkey-patch `Marshal.dump` to enable dumping of mocked or stubbed
-
# objects. By default this will not work since RSpec mocks works by
-
# adding singleton methods that cannot be serialized. This patch removes
-
# these singleton methods before serialization. Setting to falsey removes
-
# the patch.
-
#
-
# This method is idempotent.
-
1
def patch_marshal_to_support_partial_doubles=(val)
-
if val
-
RSpec::Mocks::MarshalExtension.patch!
-
else
-
RSpec::Mocks::MarshalExtension.unpatch!
-
end
-
end
-
-
# @api private
-
# Resets the configured syntax to the default.
-
1
def reset_syntaxes_to_default
-
1
self.syntax = [:should, :expect]
-
1
RSpec::Mocks::Syntax.warn_about_should!
-
end
-
end
-
-
# Mocks specific configuration, as distinct from `RSpec.configuration`
-
# which is core RSpec configuration.
-
1
def self.configuration
-
2
@configuration ||= Configuration.new
-
end
-
-
1
configuration.reset_syntaxes_to_default
-
end
-
end
-
1
RSpec::Support.require_rspec_support "object_formatter"
-
-
1
module RSpec
-
1
module Mocks
-
# Raised when a message expectation is not satisfied.
-
1
MockExpectationError = Class.new(Exception)
-
-
# Raised when a test double is used after it has been torn
-
# down (typically at the end of an rspec-core example).
-
1
ExpiredTestDoubleError = Class.new(MockExpectationError)
-
-
# Raised when doubles or partial doubles are used outside of the per-test lifecycle.
-
1
OutsideOfExampleError = Class.new(StandardError)
-
-
# Raised when an expectation customization method (e.g. `with`,
-
# `and_return`) is called on a message expectation which has already been
-
# invoked.
-
1
MockExpectationAlreadyInvokedError = Class.new(Exception)
-
-
# Raised for situations that RSpec cannot support due to mutations made
-
# externally on arguments that RSpec is holding onto to use for later
-
# comparisons.
-
#
-
# @deprecated We no longer raise this error but the constant remains until
-
# RSpec 4 for SemVer reasons.
-
1
CannotSupportArgMutationsError = Class.new(StandardError)
-
-
# @private
-
1
UnsupportedMatcherError = Class.new(StandardError)
-
# @private
-
1
NegationUnsupportedError = Class.new(StandardError)
-
# @private
-
1
VerifyingDoubleNotDefinedError = Class.new(StandardError)
-
-
# @private
-
1
class ErrorGenerator
-
1
attr_writer :opts
-
-
1
def initialize(target=nil)
-
@target = target
-
end
-
-
# @private
-
1
def opts
-
@opts ||= {}
-
end
-
-
# @private
-
1
def raise_unexpected_message_error(message, args)
-
__raise "#{intro} received unexpected message :#{message} with #{format_args(args)}"
-
end
-
-
# @private
-
1
def raise_unexpected_message_args_error(expectation, args_for_multiple_calls, source_id=nil)
-
__raise error_message(expectation, args_for_multiple_calls), nil, source_id
-
end
-
-
# @private
-
1
def raise_missing_default_stub_error(expectation, args_for_multiple_calls)
-
message = error_message(expectation, args_for_multiple_calls)
-
message << "\n Please stub a default value first if message might be received with other args as well. \n"
-
-
__raise message
-
end
-
-
# @private
-
1
def raise_similar_message_args_error(expectation, args_for_multiple_calls, backtrace_line=nil)
-
__raise error_message(expectation, args_for_multiple_calls), backtrace_line
-
end
-
-
1
def default_error_message(expectation, expected_args, actual_args)
-
"#{intro} received #{expectation.message.inspect} #{unexpected_arguments_message(expected_args, actual_args)}"
-
end
-
-
# rubocop:disable Style/ParameterLists
-
# @private
-
1
def raise_expectation_error(message, expected_received_count, argument_list_matcher,
-
actual_received_count, expectation_count_type, args,
-
backtrace_line=nil, source_id=nil)
-
expected_part = expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher)
-
received_part = received_part_of_expectation_error(actual_received_count, args)
-
__raise "(#{intro(:unwrapped)}).#{message}#{format_args(args)}\n #{expected_part}\n #{received_part}", backtrace_line, source_id
-
end
-
# rubocop:enable Style/ParameterLists
-
-
# @private
-
1
def raise_unimplemented_error(doubled_module, method_name, object)
-
message = case object
-
when InstanceVerifyingDouble
-
"the %s class does not implement the instance method: %s" <<
-
if ObjectMethodReference.for(doubled_module, method_name).implemented?
-
". Perhaps you meant to use `class_double` instead?"
-
else
-
""
-
end
-
when ClassVerifyingDouble
-
"the %s class does not implement the class method: %s" <<
-
if InstanceMethodReference.for(doubled_module, method_name).implemented?
-
". Perhaps you meant to use `instance_double` instead?"
-
else
-
""
-
end
-
else
-
"%s does not implement: %s"
-
end
-
-
__raise message % [doubled_module.description, method_name]
-
end
-
-
# @private
-
1
def raise_non_public_error(method_name, visibility)
-
raise NoMethodError, "%s method `%s' called on %s" % [
-
visibility, method_name, intro
-
]
-
end
-
-
# @private
-
1
def raise_invalid_arguments_error(verifier)
-
__raise verifier.error_message
-
end
-
-
# @private
-
1
def raise_expired_test_double_error
-
raise ExpiredTestDoubleError,
-
"#{intro} was originally created in one example but has leaked into " \
-
"another example and can no longer be used. rspec-mocks' doubles are " \
-
"designed to only last for one example, and you need to create a new " \
-
"one in each example you wish to use it for."
-
end
-
-
# @private
-
1
def describe_expectation(verb, message, expected_received_count, _actual_received_count, args)
-
"#{verb} #{message}#{format_args(args)} #{count_message(expected_received_count)}"
-
end
-
-
# @private
-
1
def raise_out_of_order_error(message)
-
__raise "#{intro} received :#{message} out of order"
-
end
-
-
# @private
-
1
def raise_missing_block_error(args_to_yield)
-
__raise "#{intro} asked to yield |#{arg_list(args_to_yield)}| but no block was passed"
-
end
-
-
# @private
-
1
def raise_wrong_arity_error(args_to_yield, signature)
-
__raise "#{intro} yielded |#{arg_list(args_to_yield)}| to block with #{signature.description}"
-
end
-
-
# @private
-
1
def raise_only_valid_on_a_partial_double(method)
-
__raise "#{intro} is a pure test double. `#{method}` is only " \
-
"available on a partial double."
-
end
-
-
# @private
-
1
def raise_expectation_on_unstubbed_method(method)
-
__raise "#{intro} expected to have received #{method}, but that " \
-
"object is not a spy or method has not been stubbed."
-
end
-
-
# @private
-
1
def raise_expectation_on_mocked_method(method)
-
__raise "#{intro} expected to have received #{method}, but that " \
-
"method has been mocked instead of stubbed or spied."
-
end
-
-
# @private
-
1
def raise_double_negation_error(wrapped_expression)
-
__raise "Isn't life confusing enough? You've already set a " \
-
"negative message expectation and now you are trying to " \
-
"negate it again with `never`. What does an expression like " \
-
"`#{wrapped_expression}.not_to receive(:msg).never` even mean?"
-
end
-
-
# @private
-
1
def raise_verifying_double_not_defined_error(ref)
-
notify(VerifyingDoubleNotDefinedError.new(
-
"#{ref.description.inspect} is not a defined constant. " \
-
"Perhaps you misspelt it? " \
-
"Disable check with `verify_doubled_constant_names` configuration option."
-
))
-
end
-
-
# @private
-
1
def raise_have_received_disallowed(type, reason)
-
__raise "Using #{type}(...) with the `have_received` " \
-
"matcher is not supported#{reason}."
-
end
-
-
# @private
-
1
def raise_cant_constrain_count_for_negated_have_received_error(count_constraint)
-
__raise "can't use #{count_constraint} when negative"
-
end
-
-
# @private
-
1
def raise_method_not_stubbed_error(method_name)
-
__raise "The method `#{method_name}` was not stubbed or was already unstubbed"
-
end
-
-
# @private
-
1
def raise_already_invoked_error(message, calling_customization)
-
error_message = "The message expectation for #{intro}.#{message} has already been invoked " \
-
"and cannot be modified further (e.g. using `#{calling_customization}`). All message expectation " \
-
"customizations must be applied before it is used for the first time."
-
-
notify MockExpectationAlreadyInvokedError.new(error_message)
-
end
-
-
1
def raise_expectation_on_nil_error(method_name)
-
__raise expectation_on_nil_message(method_name)
-
end
-
-
1
def expectation_on_nil_message(method_name)
-
"An expectation of `:#{method_name}` was set on `nil`. " \
-
"To allow expectations on `nil` and suppress this message, set `config.allow_message_expectations_on_nil` to `true`. " \
-
"To disallow expectations on `nil`, set `config.allow_message_expectations_on_nil` to `false`"
-
end
-
-
# @private
-
1
def intro(unwrapped=false)
-
case @target
-
when TestDouble then TestDoubleFormatter.format(@target, unwrapped)
-
when Class then
-
formatted = "#{@target.inspect} (class)"
-
return formatted if unwrapped
-
"#<#{formatted}>"
-
when NilClass then "nil"
-
else @target.inspect
-
end
-
end
-
-
# @private
-
1
def method_call_args_description(args, generic_prefix=" with arguments: ", matcher_prefix=" with ")
-
case args.first
-
when ArgumentMatchers::AnyArgsMatcher then "#{matcher_prefix}any arguments"
-
when ArgumentMatchers::NoArgsMatcher then "#{matcher_prefix}no arguments"
-
else
-
if yield
-
"#{generic_prefix}#{format_args(args)}"
-
else
-
""
-
end
-
end
-
end
-
-
1
private
-
-
1
def received_part_of_expectation_error(actual_received_count, args)
-
"received: #{count_message(actual_received_count)}" +
-
method_call_args_description(args) do
-
actual_received_count > 0 && args.length > 0
-
end
-
end
-
-
1
def expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher)
-
"expected: #{count_message(expected_received_count, expectation_count_type)}" +
-
method_call_args_description(argument_list_matcher.expected_args) do
-
argument_list_matcher.expected_args.length > 0
-
end
-
end
-
-
1
def unexpected_arguments_message(expected_args_string, actual_args_string)
-
"with unexpected arguments\n expected: #{expected_args_string}\n got: #{actual_args_string}"
-
end
-
-
1
def error_message(expectation, args_for_multiple_calls)
-
expected_args = format_args(expectation.expected_args)
-
actual_args = format_received_args(args_for_multiple_calls)
-
message = default_error_message(expectation, expected_args, actual_args)
-
-
if args_for_multiple_calls.one?
-
diff = diff_message(expectation.expected_args, args_for_multiple_calls.first)
-
message << "\nDiff:#{diff}" unless diff.strip.empty?
-
end
-
-
message
-
end
-
-
1
def diff_message(expected_args, actual_args)
-
formatted_expected_args = expected_args.map do |x|
-
RSpec::Support.rspec_description_for_object(x)
-
end
-
-
formatted_expected_args, actual_args = unpack_string_args(formatted_expected_args, actual_args)
-
-
differ.diff(actual_args, formatted_expected_args)
-
end
-
-
1
def unpack_string_args(formatted_expected_args, actual_args)
-
if [formatted_expected_args, actual_args].all? { |x| list_of_exactly_one_string?(x) }
-
[formatted_expected_args.first, actual_args.first]
-
else
-
[formatted_expected_args, actual_args]
-
end
-
end
-
-
1
def list_of_exactly_one_string?(args)
-
Array === args && args.count == 1 && String === args.first
-
end
-
-
1
def differ
-
RSpec::Support::Differ.new(:color => RSpec::Mocks.configuration.color?)
-
end
-
-
1
def __raise(message, backtrace_line=nil, source_id=nil)
-
message = opts[:message] unless opts[:message].nil?
-
exception = RSpec::Mocks::MockExpectationError.new(message)
-
prepend_to_backtrace(exception, backtrace_line) if backtrace_line
-
notify exception, :source_id => source_id
-
end
-
-
1
if RSpec::Support::Ruby.jruby?
-
def prepend_to_backtrace(exception, line)
-
raise exception
-
rescue RSpec::Mocks::MockExpectationError => with_backtrace
-
with_backtrace.backtrace.unshift(line)
-
end
-
else
-
1
def prepend_to_backtrace(exception, line)
-
exception.set_backtrace(caller.unshift line)
-
end
-
end
-
-
1
def notify(*args)
-
RSpec::Support.notify_failure(*args)
-
end
-
-
1
def format_args(args)
-
return "(no args)" if args.empty?
-
"(#{arg_list(args)})"
-
end
-
-
1
def arg_list(args)
-
args.map { |arg| RSpec::Support::ObjectFormatter.format(arg) }.join(", ")
-
end
-
-
1
def format_received_args(args_for_multiple_calls)
-
grouped_args(args_for_multiple_calls).map do |args_for_one_call, index|
-
"#{format_args(args_for_one_call)}#{group_count(index, args_for_multiple_calls)}"
-
end.join("\n ")
-
end
-
-
1
def count_message(count, expectation_count_type=nil)
-
return "at least #{times(count.abs)}" if count < 0 || expectation_count_type == :at_least
-
return "at most #{times(count)}" if expectation_count_type == :at_most
-
times(count)
-
end
-
-
1
def times(count)
-
"#{count} time#{count == 1 ? '' : 's'}"
-
end
-
-
1
def grouped_args(args)
-
Hash[args.group_by { |x| x }.map { |k, v| [k, v.count] }]
-
end
-
-
1
def group_count(index, args)
-
" (#{times(index)})" if args.size > 1 || index > 1
-
end
-
end
-
-
# @private
-
1
def self.error_generator
-
@error_generator ||= ErrorGenerator.new
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_mocks 'object_reference'
-
-
1
module RSpec
-
1
module Mocks
-
# Contains methods intended to be used from within code examples.
-
# Mix this in to your test context (such as a test framework base class)
-
# to use rspec-mocks with your test framework. If you're using rspec-core,
-
# it'll take care of doing this for you.
-
1
module ExampleMethods
-
1
include RSpec::Mocks::ArgumentMatchers
-
-
# @overload double()
-
# @overload double(name)
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload double(stubs)
-
# @param stubs (Hash) hash of message/return-value pairs
-
# @overload double(name, stubs)
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs (Hash) hash of message/return-value pairs
-
# @return (Double)
-
#
-
# Constructs an instance of [RSpec::Mocks::Double](RSpec::Mocks::Double) configured
-
# with an optional name, used for reporting in failure messages, and an optional
-
# hash of message/return-value pairs.
-
#
-
# @example
-
# book = double("book", :title => "The RSpec Book")
-
# book.title #=> "The RSpec Book"
-
#
-
# card = double("card", :suit => "Spades", :rank => "A")
-
# card.suit #=> "Spades"
-
# card.rank #=> "A"
-
#
-
1
def double(*args)
-
ExampleMethods.declare_double(Double, *args)
-
end
-
-
# @overload instance_double(doubled_class)
-
# @param doubled_class [String, Class]
-
# @overload instance_double(doubled_class, name)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload instance_double(doubled_class, stubs)
-
# @param doubled_class [String, Class]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload instance_double(doubled_class, name, stubs)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return InstanceVerifyingDouble
-
#
-
# Constructs a test double against a specific class. If the given class
-
# name has been loaded, only instance methods defined on the class are
-
# allowed to be stubbed. In all other ways it behaves like a
-
# [double](double).
-
1
def instance_double(doubled_class, *args)
-
ref = ObjectReference.for(doubled_class)
-
ExampleMethods.declare_verifying_double(InstanceVerifyingDouble, ref, *args)
-
end
-
-
# @overload class_double(doubled_class)
-
# @param doubled_class [String, Module]
-
# @overload class_double(doubled_class, name)
-
# @param doubled_class [String, Module]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload class_double(doubled_class, stubs)
-
# @param doubled_class [String, Module]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload class_double(doubled_class, name, stubs)
-
# @param doubled_class [String, Module]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return ClassVerifyingDouble
-
#
-
# Constructs a test double against a specific class. If the given class
-
# name has been loaded, only class methods defined on the class are
-
# allowed to be stubbed. In all other ways it behaves like a
-
# [double](double).
-
1
def class_double(doubled_class, *args)
-
ref = ObjectReference.for(doubled_class)
-
ExampleMethods.declare_verifying_double(ClassVerifyingDouble, ref, *args)
-
end
-
-
# @overload object_double(object_or_name)
-
# @param object_or_name [String, Object]
-
# @overload object_double(object_or_name, name)
-
# @param object_or_name [String, Object]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload object_double(object_or_name, stubs)
-
# @param object_or_name [String, Object]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload object_double(object_or_name, name, stubs)
-
# @param object_or_name [String, Object]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return ObjectVerifyingDouble
-
#
-
# Constructs a test double against a specific object. Only the methods
-
# the object responds to are allowed to be stubbed. If a String argument
-
# is provided, it is assumed to reference a constant object which is used
-
# for verification. In all other ways it behaves like a [double](double).
-
1
def object_double(object_or_name, *args)
-
ref = ObjectReference.for(object_or_name, :allow_direct_object_refs)
-
ExampleMethods.declare_verifying_double(ObjectVerifyingDouble, ref, *args)
-
end
-
-
# @overload spy()
-
# @overload spy(name)
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload spy(stubs)
-
# @param stubs (Hash) hash of message/return-value pairs
-
# @overload spy(name, stubs)
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs (Hash) hash of message/return-value pairs
-
# @return (Double)
-
#
-
# Constructs a test double that is optimized for use with
-
# `have_received`. With a normal double one has to stub methods in order
-
# to be able to spy them. A spy automatically spies on all methods.
-
1
def spy(*args)
-
double(*args).as_null_object
-
end
-
-
# @overload instance_spy(doubled_class)
-
# @param doubled_class [String, Class]
-
# @overload instance_spy(doubled_class, name)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload instance_spy(doubled_class, stubs)
-
# @param doubled_class [String, Class]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload instance_spy(doubled_class, name, stubs)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return InstanceVerifyingDouble
-
#
-
# Constructs a test double that is optimized for use with `have_received`
-
# against a specific class. If the given class name has been loaded, only
-
# instance methods defined on the class are allowed to be stubbed. With
-
# a normal double one has to stub methods in order to be able to spy
-
# them. An instance_spy automatically spies on all instance methods to
-
# which the class responds.
-
1
def instance_spy(*args)
-
instance_double(*args).as_null_object
-
end
-
-
# @overload object_spy(object_or_name)
-
# @param object_or_name [String, Object]
-
# @overload object_spy(object_or_name, name)
-
# @param object_or_name [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload object_spy(object_or_name, stubs)
-
# @param object_or_name [String, Object]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload object_spy(object_or_name, name, stubs)
-
# @param object_or_name [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return ObjectVerifyingDouble
-
#
-
# Constructs a test double that is optimized for use with `have_received`
-
# against a specific object. Only instance methods defined on the object
-
# are allowed to be stubbed. With a normal double one has to stub
-
# methods in order to be able to spy them. An object_spy automatically
-
# spies on all methods to which the object responds.
-
1
def object_spy(*args)
-
object_double(*args).as_null_object
-
end
-
-
# @overload class_spy(doubled_class)
-
# @param doubled_class [String, Module]
-
# @overload class_spy(doubled_class, name)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @overload class_spy(doubled_class, stubs)
-
# @param doubled_class [String, Module]
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @overload class_spy(doubled_class, name, stubs)
-
# @param doubled_class [String, Class]
-
# @param name [String/Symbol] name or description to be used in failure messages
-
# @param stubs [Hash] hash of message/return-value pairs
-
# @return ClassVerifyingDouble
-
#
-
# Constructs a test double that is optimized for use with `have_received`
-
# against a specific class. If the given class name has been loaded,
-
# only class methods defined on the class are allowed to be stubbed.
-
# With a normal double one has to stub methods in order to be able to spy
-
# them. An class_spy automatically spies on all class methods to which the
-
# class responds.
-
1
def class_spy(*args)
-
class_double(*args).as_null_object
-
end
-
-
# Disables warning messages about expectations being set on nil.
-
#
-
# By default warning messages are issued when expectations are set on
-
# nil. This is to prevent false-positives and to catch potential bugs
-
# early on.
-
# @deprecated Use {RSpec::Mocks::Configuration#allow_message_expectations_on_nil} instead.
-
1
def allow_message_expectations_on_nil
-
RSpec::Mocks.space.proxy_for(nil).warn_about_expectations = false
-
end
-
-
# Stubs the named constant with the given value.
-
# Like method stubs, the constant will be restored
-
# to its original value (or lack of one, if it was
-
# undefined) when the example completes.
-
#
-
# @param constant_name [String] The fully qualified name of the constant. The current
-
# constant scoping at the point of call is not considered.
-
# @param value [Object] The value to make the constant refer to. When the
-
# example completes, the constant will be restored to its prior state.
-
# @param options [Hash] Stubbing options.
-
# @option options :transfer_nested_constants [Boolean, Array<Symbol>] Determines
-
# what nested constants, if any, will be transferred from the original value
-
# of the constant to the new value of the constant. This only works if both
-
# the original and new values are modules (or classes).
-
# @return [Object] the stubbed value of the constant
-
#
-
# @example
-
# stub_const("MyClass", Class.new) # => Replaces (or defines) MyClass with a new class object.
-
# stub_const("SomeModel::PER_PAGE", 5) # => Sets SomeModel::PER_PAGE to 5.
-
#
-
# class CardDeck
-
# SUITS = [:Spades, :Diamonds, :Clubs, :Hearts]
-
# NUM_CARDS = 52
-
# end
-
#
-
# stub_const("CardDeck", Class.new)
-
# CardDeck::SUITS # => uninitialized constant error
-
# CardDeck::NUM_CARDS # => uninitialized constant error
-
#
-
# stub_const("CardDeck", Class.new, :transfer_nested_constants => true)
-
# CardDeck::SUITS # => our suits array
-
# CardDeck::NUM_CARDS # => 52
-
#
-
# stub_const("CardDeck", Class.new, :transfer_nested_constants => [:SUITS])
-
# CardDeck::SUITS # => our suits array
-
# CardDeck::NUM_CARDS # => uninitialized constant error
-
1
def stub_const(constant_name, value, options={})
-
ConstantMutator.stub(constant_name, value, options)
-
end
-
-
# Hides the named constant with the given value. The constant will be
-
# undefined for the duration of the test.
-
#
-
# Like method stubs, the constant will be restored to its original value
-
# when the example completes.
-
#
-
# @param constant_name [String] The fully qualified name of the constant.
-
# The current constant scoping at the point of call is not considered.
-
#
-
# @example
-
# hide_const("MyClass") # => MyClass is now an undefined constant
-
1
def hide_const(constant_name)
-
ConstantMutator.hide(constant_name)
-
end
-
-
# Verifies that the given object received the expected message during the
-
# course of the test. On a spy objects or as null object doubles this
-
# works for any method, on other objects the method must have
-
# been stubbed beforehand in order for messages to be verified.
-
#
-
# Stubbing and verifying messages received in this way implements the
-
# Test Spy pattern.
-
#
-
# @param method_name [Symbol] name of the method expected to have been
-
# called.
-
#
-
# @example
-
# invitation = double('invitation', accept: true)
-
# user.accept_invitation(invitation)
-
# expect(invitation).to have_received(:accept)
-
#
-
# # You can also use most message expectations:
-
# expect(invitation).to have_received(:accept).with(mailer).once
-
#
-
# @note `have_received(...).with(...)` is unable to work properly when
-
# passed arguments are mutated after the spy records the received message.
-
1
def have_received(method_name, &block)
-
Matchers::HaveReceived.new(method_name, &block)
-
end
-
-
# @method expect
-
# Used to wrap an object in preparation for setting a mock expectation
-
# on it.
-
#
-
# @example
-
# expect(obj).to receive(:foo).with(5).and_return(:return_value)
-
#
-
# @note This method is usually provided by rspec-expectations. However,
-
# if you use rspec-mocks without rspec-expectations, there's a definition
-
# of it that is made available here. If you disable the `:expect` syntax
-
# this method will be undefined.
-
-
# @method allow
-
# Used to wrap an object in preparation for stubbing a method
-
# on it.
-
#
-
# @example
-
# allow(dbl).to receive(:foo).with(5).and_return(:return_value)
-
#
-
# @note If you disable the `:expect` syntax this method will be undefined.
-
-
# @method expect_any_instance_of
-
# Used to wrap a class in preparation for setting a mock expectation
-
# on instances of it.
-
#
-
# @example
-
# expect_any_instance_of(MyClass).to receive(:foo)
-
#
-
# @note If you disable the `:expect` syntax this method will be undefined.
-
-
# @method allow_any_instance_of
-
# Used to wrap a class in preparation for stubbing a method
-
# on instances of it.
-
#
-
# @example
-
# allow_any_instance_of(MyClass).to receive(:foo)
-
#
-
# @note This is only available when you have enabled the `expect` syntax.
-
-
# @method receive
-
# Used to specify a message that you expect or allow an object
-
# to receive. The object returned by `receive` supports the same
-
# fluent interface that `should_receive` and `stub` have always
-
# supported, allowing you to constrain the arguments or number of
-
# times, and configure how the object should respond to the message.
-
#
-
# @example
-
# expect(obj).to receive(:hello).with("world").exactly(3).times
-
#
-
# @note If you disable the `:expect` syntax this method will be undefined.
-
-
# @method receive_messages
-
# Shorthand syntax used to setup message(s), and their return value(s),
-
# that you expect or allow an object to receive. The method takes a hash
-
# of messages and their respective return values. Unlike with `receive`,
-
# you cannot apply further customizations using a block or the fluent
-
# interface.
-
#
-
# @example
-
# allow(obj).to receive_messages(:speak => "Hello World")
-
# allow(obj).to receive_messages(:speak => "Hello", :meow => "Meow")
-
#
-
# @note If you disable the `:expect` syntax this method will be undefined.
-
-
# @method receive_message_chain
-
# @overload receive_message_chain(method1, method2)
-
# @overload receive_message_chain("method1.method2")
-
# @overload receive_message_chain(method1, method_to_value_hash)
-
#
-
# stubs/mocks a chain of messages on an object or test double.
-
#
-
# ## Warning:
-
#
-
# Chains can be arbitrarily long, which makes it quite painless to
-
# violate the Law of Demeter in violent ways, so you should consider any
-
# use of `receive_message_chain` a code smell. Even though not all code smells
-
# indicate real problems (think fluent interfaces), `receive_message_chain` still
-
# results in brittle examples. For example, if you write
-
# `allow(foo).to receive_message_chain(:bar, :baz => 37)` in a spec and then the
-
# implementation calls `foo.baz.bar`, the stub will not work.
-
#
-
# @example
-
# allow(double).to receive_message_chain("foo.bar") { :baz }
-
# allow(double).to receive_message_chain(:foo, :bar => :baz)
-
# allow(double).to receive_message_chain(:foo, :bar) { :baz }
-
#
-
# # Given any of ^^ these three forms ^^:
-
# double.foo.bar # => :baz
-
#
-
# # Common use in Rails/ActiveRecord:
-
# allow(Article).to receive_message_chain("recent.published") { [Article.new] }
-
#
-
# @note If you disable the `:expect` syntax this method will be undefined.
-
-
# @private
-
1
def self.included(klass)
-
1
klass.class_exec do
-
# This gets mixed in so that if `RSpec::Matchers` is included in
-
# `klass` later, it's definition of `expect` will take precedence.
-
1
include ExpectHost unless method_defined?(:expect)
-
end
-
end
-
-
# @private
-
1
def self.extended(object)
-
# This gets extended in so that if `RSpec::Matchers` is included in
-
# `klass` later, it's definition of `expect` will take precedence.
-
object.extend ExpectHost unless object.respond_to?(:expect)
-
end
-
-
# @private
-
1
def self.declare_verifying_double(type, ref, *args)
-
if RSpec::Mocks.configuration.verify_doubled_constant_names? &&
-
!ref.defined?
-
-
RSpec::Mocks.error_generator.raise_verifying_double_not_defined_error(ref)
-
end
-
-
RSpec::Mocks.configuration.verifying_double_callbacks.each do |block|
-
block.call(ref)
-
end
-
-
declare_double(type, ref, *args)
-
end
-
-
# @private
-
1
def self.declare_double(type, *args)
-
args << {} unless Hash === args.last
-
type.new(*args)
-
end
-
-
# This module exists to host the `expect` method for cases where
-
# rspec-mocks is used w/o rspec-expectations.
-
1
module ExpectHost
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class InstanceMethodStasher
-
1
def initialize(object, method)
-
@object = object
-
@method = method
-
@klass = (class << object; self; end)
-
-
@original_method = nil
-
@method_is_stashed = false
-
end
-
-
1
attr_reader :original_method
-
-
1
if RUBY_VERSION.to_f < 1.9
-
# @private
-
def method_is_stashed?
-
@method_is_stashed
-
end
-
-
# @private
-
def stash
-
return if !method_defined_directly_on_klass? || @method_is_stashed
-
-
@klass.__send__(:alias_method, stashed_method_name, @method)
-
@method_is_stashed = true
-
end
-
-
# @private
-
def stashed_method_name
-
"obfuscated_by_rspec_mocks__#{@method}"
-
end
-
-
# @private
-
def restore
-
return unless @method_is_stashed
-
-
if @klass.__send__(:method_defined?, @method)
-
@klass.__send__(:undef_method, @method)
-
end
-
@klass.__send__(:alias_method, @method, stashed_method_name)
-
@klass.__send__(:remove_method, stashed_method_name)
-
@method_is_stashed = false
-
end
-
else
-
-
# @private
-
1
def method_is_stashed?
-
!!@original_method
-
end
-
-
# @private
-
1
def stash
-
return unless method_defined_directly_on_klass?
-
@original_method ||= ::RSpec::Support.method_handle_for(@object, @method)
-
@klass.__send__(:undef_method, @method)
-
end
-
-
# @private
-
1
def restore
-
return unless @original_method
-
-
if @klass.__send__(:method_defined?, @method)
-
@klass.__send__(:undef_method, @method)
-
end
-
-
handle_restoration_failures do
-
@klass.__send__(:define_method, @method, @original_method)
-
end
-
-
@original_method = nil
-
end
-
end
-
-
1
if RUBY_DESCRIPTION.include?('2.0.0p247') || RUBY_DESCRIPTION.include?('2.0.0p195')
-
# ruby 2.0.0-p247 and 2.0.0-p195 both have a bug that we can't work around :(.
-
# https://bugs.ruby-lang.org/issues/8686
-
def handle_restoration_failures
-
yield
-
rescue TypeError
-
RSpec.warn_with(
-
"RSpec failed to properly restore a partial double (#{@object.inspect}) " \
-
"to its original state due to a known bug in MRI 2.0.0-p195 & p247 " \
-
"(https://bugs.ruby-lang.org/issues/8686). This object may remain " \
-
"screwed up for the rest of this process. Please upgrade to 2.0.0-p353 or above.",
-
:call_site => nil, :use_spec_location_as_call_site => true
-
)
-
end
-
else
-
1
def handle_restoration_failures
-
# No known reasons for restoration to fail on other rubies.
-
yield
-
end
-
end
-
-
1
private
-
-
# @private
-
1
def method_defined_directly_on_klass?
-
method_defined_on_klass? && method_owned_by_klass?
-
end
-
-
# @private
-
1
def method_defined_on_klass?(klass=@klass)
-
MethodReference.method_defined_at_any_visibility?(klass, @method)
-
end
-
-
1
def method_owned_by_klass?
-
owner = @klass.instance_method(@method).owner
-
-
# On Ruby 2.0.0+ the owner of a method on a class which has been
-
# `prepend`ed may actually be an instance, e.g.
-
# `#<MyClass:0x007fbb94e3cd10>`, rather than the expected `MyClass`.
-
owner = owner.class unless Module === owner
-
-
# On some 1.9s (e.g. rubinius) aliased methods
-
# can report the wrong owner. Example:
-
# class MyClass
-
# class << self
-
# alias alternate_new new
-
# end
-
# end
-
#
-
# MyClass.owner(:alternate_new) returns `Class` when incorrect,
-
# but we need to consider the owner to be `MyClass` because
-
# it is not actually available on `Class` but is on `MyClass`.
-
# Hence, we verify that the owner actually has the method defined.
-
# If the given owner does not have the method defined, we assume
-
# that the method is actually owned by @klass.
-
owner == @klass || !(method_defined_on_klass?(owner))
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# A message expectation that only allows concrete return values to be set
-
# for a message. While this same effect can be achieved using a standard
-
# MessageExpectation, this version is much faster and so can be used as an
-
# optimization.
-
#
-
# @private
-
1
class SimpleMessageExpectation
-
1
def initialize(message, response, error_generator, backtrace_line=nil)
-
@message, @response, @error_generator, @backtrace_line = message.to_sym, response, error_generator, backtrace_line
-
@received = false
-
end
-
-
1
def invoke(*_)
-
@received = true
-
@response
-
end
-
-
1
def matches?(message, *_)
-
@message == message.to_sym
-
end
-
-
1
def called_max_times?
-
false
-
end
-
-
1
def verify_messages_received
-
return if @received
-
@error_generator.raise_expectation_error(
-
@message, 1, ArgumentListMatcher::MATCH_ALL, 0, nil, [], @backtrace_line
-
)
-
end
-
-
1
def unadvise(_)
-
end
-
end
-
-
# Represents an individual method stub or message expectation. The methods
-
# defined here can be used to configure how it behaves. The methods return
-
# `self` so that they can be chained together to form a fluent interface.
-
1
class MessageExpectation
-
# @!group Configuring Responses
-
-
# @overload and_return(value)
-
# @overload and_return(first_value, second_value)
-
#
-
# Tells the object to return a value when it receives the message. Given
-
# more than one value, the first value is returned the first time the
-
# message is received, the second value is returned the next time, etc,
-
# etc.
-
#
-
# If the message is received more times than there are values, the last
-
# value is received for every subsequent call.
-
#
-
# @return [nil] No further chaining is supported after this.
-
# @example
-
# allow(counter).to receive(:count).and_return(1)
-
# counter.count # => 1
-
# counter.count # => 1
-
#
-
# allow(counter).to receive(:count).and_return(1,2,3)
-
# counter.count # => 1
-
# counter.count # => 2
-
# counter.count # => 3
-
# counter.count # => 3
-
# counter.count # => 3
-
# # etc
-
1
def and_return(first_value, *values)
-
raise_already_invoked_error_if_necessary(__method__)
-
if negative?
-
raise "`and_return` is not supported with negative message expectations"
-
end
-
-
if block_given?
-
raise ArgumentError, "Implementation blocks aren't supported with `and_return`"
-
end
-
-
values.unshift(first_value)
-
@expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 && @at_least)
-
self.terminal_implementation_action = AndReturnImplementation.new(values)
-
-
nil
-
end
-
-
# Tells the object to delegate to the original unmodified method
-
# when it receives the message.
-
#
-
# @note This is only available on partial doubles.
-
#
-
# @return [nil] No further chaining is supported after this.
-
# @example
-
# expect(counter).to receive(:increment).and_call_original
-
# original_count = counter.count
-
# counter.increment
-
# expect(counter.count).to eq(original_count + 1)
-
1
def and_call_original
-
wrap_original(__method__) do |original, *args, &block|
-
original.call(*args, &block)
-
end
-
end
-
-
# Decorates the stubbed method with the supplied block. The original
-
# unmodified method is passed to the block along with any method call
-
# arguments so you can delegate to it, whilst still being able to
-
# change what args are passed to it and/or change the return value.
-
#
-
# @note This is only available on partial doubles.
-
#
-
# @return [nil] No further chaining is supported after this.
-
# @example
-
# expect(api).to receive(:large_list).and_wrap_original do |original_method, *args, &block|
-
# original_method.call(*args, &block).first(10)
-
# end
-
1
def and_wrap_original(&block)
-
wrap_original(__method__, &block)
-
end
-
-
# @overload and_raise
-
# @overload and_raise(ExceptionClass)
-
# @overload and_raise(ExceptionClass, message)
-
# @overload and_raise(exception_instance)
-
#
-
# Tells the object to raise an exception when the message is received.
-
#
-
# @return [nil] No further chaining is supported after this.
-
# @note
-
# When you pass an exception class, the MessageExpectation will raise
-
# an instance of it, creating it with `exception` and passing `message`
-
# if specified. If the exception class initializer requires more than
-
# one parameters, you must pass in an instance and not the class,
-
# otherwise this method will raise an ArgumentError exception.
-
#
-
# @example
-
# allow(car).to receive(:go).and_raise
-
# allow(car).to receive(:go).and_raise(OutOfGas)
-
# allow(car).to receive(:go).and_raise(OutOfGas, "At least 2 oz of gas needed to drive")
-
# allow(car).to receive(:go).and_raise(OutOfGas.new(2, :oz))
-
1
def and_raise(*args)
-
raise_already_invoked_error_if_necessary(__method__)
-
self.terminal_implementation_action = Proc.new { raise(*args) }
-
nil
-
end
-
-
# @overload and_throw(symbol)
-
# @overload and_throw(symbol, object)
-
#
-
# Tells the object to throw a symbol (with the object if that form is
-
# used) when the message is received.
-
#
-
# @return [nil] No further chaining is supported after this.
-
# @example
-
# allow(car).to receive(:go).and_throw(:out_of_gas)
-
# allow(car).to receive(:go).and_throw(:out_of_gas, :level => 0.1)
-
1
def and_throw(*args)
-
raise_already_invoked_error_if_necessary(__method__)
-
self.terminal_implementation_action = Proc.new { throw(*args) }
-
nil
-
end
-
-
# Tells the object to yield one or more args to a block when the message
-
# is received.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# stream.stub(:open).and_yield(StringIO.new)
-
1
def and_yield(*args, &block)
-
raise_already_invoked_error_if_necessary(__method__)
-
yield @eval_context = Object.new if block
-
-
# Initialize args to yield now that it's being used, see also: comment
-
# in constructor.
-
@args_to_yield ||= []
-
-
@args_to_yield << args
-
self.initial_implementation_action = AndYieldImplementation.new(@args_to_yield, @eval_context, @error_generator)
-
self
-
end
-
# @!endgroup
-
-
# @!group Constraining Receive Counts
-
-
# Constrain a message expectation to be received a specific number of
-
# times.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(dealer).to receive(:deal_card).exactly(10).times
-
1
def exactly(n, &block)
-
raise_already_invoked_error_if_necessary(__method__)
-
self.inner_implementation_action = block
-
set_expected_received_count :exactly, n
-
self
-
end
-
-
# Constrain a message expectation to be received at least a specific
-
# number of times.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(dealer).to receive(:deal_card).at_least(9).times
-
1
def at_least(n, &block)
-
raise_already_invoked_error_if_necessary(__method__)
-
set_expected_received_count :at_least, n
-
-
if n == 0
-
raise "at_least(0) has been removed, use allow(...).to receive(:message) instead"
-
end
-
-
self.inner_implementation_action = block
-
-
self
-
end
-
-
# Constrain a message expectation to be received at most a specific
-
# number of times.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(dealer).to receive(:deal_card).at_most(10).times
-
1
def at_most(n, &block)
-
raise_already_invoked_error_if_necessary(__method__)
-
self.inner_implementation_action = block
-
set_expected_received_count :at_most, n
-
self
-
end
-
-
# Syntactic sugar for `exactly`, `at_least` and `at_most`
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(dealer).to receive(:deal_card).exactly(10).times
-
# expect(dealer).to receive(:deal_card).at_least(10).times
-
# expect(dealer).to receive(:deal_card).at_most(10).times
-
1
def times(&block)
-
self.inner_implementation_action = block
-
self
-
end
-
-
# Expect a message not to be received at all.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(car).to receive(:stop).never
-
1
def never
-
error_generator.raise_double_negation_error("expect(obj)") if negative?
-
@expected_received_count = 0
-
self
-
end
-
-
# Expect a message to be received exactly one time.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(car).to receive(:go).once
-
1
def once(&block)
-
self.inner_implementation_action = block
-
set_expected_received_count :exactly, 1
-
self
-
end
-
-
# Expect a message to be received exactly two times.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(car).to receive(:go).twice
-
1
def twice(&block)
-
self.inner_implementation_action = block
-
set_expected_received_count :exactly, 2
-
self
-
end
-
-
# Expect a message to be received exactly three times.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(car).to receive(:go).thrice
-
1
def thrice(&block)
-
self.inner_implementation_action = block
-
set_expected_received_count :exactly, 3
-
self
-
end
-
# @!endgroup
-
-
# @!group Other Constraints
-
-
# Constrains a stub or message expectation to invocations with specific
-
# arguments.
-
#
-
# With a stub, if the message might be received with other args as well,
-
# you should stub a default value first, and then stub or mock the same
-
# message using `with` to constrain to specific arguments.
-
#
-
# A message expectation will fail if the message is received with different
-
# arguments.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# allow(cart).to receive(:add) { :failure }
-
# allow(cart).to receive(:add).with(Book.new(:isbn => 1934356379)) { :success }
-
# cart.add(Book.new(:isbn => 1234567890))
-
# # => :failure
-
# cart.add(Book.new(:isbn => 1934356379))
-
# # => :success
-
#
-
# expect(cart).to receive(:add).with(Book.new(:isbn => 1934356379)) { :success }
-
# cart.add(Book.new(:isbn => 1234567890))
-
# # => failed expectation
-
# cart.add(Book.new(:isbn => 1934356379))
-
# # => passes
-
1
def with(*args, &block)
-
raise_already_invoked_error_if_necessary(__method__)
-
if args.empty?
-
raise ArgumentError,
-
"`with` must have at least one argument. Use `no_args` matcher to set the expectation of receiving no arguments."
-
end
-
-
self.inner_implementation_action = block
-
@argument_list_matcher = ArgumentListMatcher.new(*args)
-
self
-
end
-
-
# Expect messages to be received in a specific order.
-
#
-
# @return [MessageExpectation] self, to support further chaining.
-
# @example
-
# expect(api).to receive(:prepare).ordered
-
# expect(api).to receive(:run).ordered
-
# expect(api).to receive(:finish).ordered
-
1
def ordered(&block)
-
if type == :stub
-
RSpec.warning(
-
"`allow(...).to receive(..).ordered` is not supported and will" \
-
"have no effect, use `and_return(*ordered_values)` instead."
-
)
-
end
-
-
self.inner_implementation_action = block
-
additional_expected_calls.times do
-
@order_group.register(self)
-
end
-
@ordered = true
-
self
-
end
-
-
# @return [String] a nice representation of the message expectation
-
1
def to_s
-
args_description = error_generator.method_call_args_description(@argument_list_matcher.expected_args, "", "") { true }
-
args_description = "(#{args_description})" unless args_description.start_with?("(")
-
"#<#{self.class} #{error_generator.intro}.#{message}#{args_description}>"
-
end
-
1
alias inspect to_s
-
-
# @private
-
# Contains the parts of `MessageExpectation` that aren't part of
-
# rspec-mocks' public API. The class is very big and could really use
-
# some collaborators it delegates to for this stuff but for now this was
-
# the simplest way to split the public from private stuff to make it
-
# easier to publish the docs for the APIs we want published.
-
1
module ImplementationDetails
-
1
attr_accessor :error_generator, :implementation
-
1
attr_reader :message
-
1
attr_reader :orig_object
-
1
attr_writer :expected_received_count, :expected_from, :argument_list_matcher
-
1
protected :expected_received_count=, :expected_from=, :error_generator, :error_generator=, :implementation=
-
-
# @private
-
1
attr_reader :type
-
-
# rubocop:disable Style/ParameterLists
-
1
def initialize(error_generator, expectation_ordering, expected_from, method_double,
-
type=:expectation, opts={}, &implementation_block)
-
@type = type
-
@error_generator = error_generator
-
@error_generator.opts = opts
-
@expected_from = expected_from
-
@method_double = method_double
-
@orig_object = @method_double.object
-
@message = @method_double.method_name
-
@actual_received_count = 0
-
@expected_received_count = type == :expectation ? 1 : :any
-
@argument_list_matcher = ArgumentListMatcher::MATCH_ALL
-
@order_group = expectation_ordering
-
@order_group.register(self) unless type == :stub
-
@expectation_type = type
-
@ordered = false
-
@at_least = @at_most = @exactly = nil
-
-
# Initialized to nil so that we don't allocate an array for every
-
# mock or stub. See also comment in `and_yield`.
-
@args_to_yield = nil
-
@eval_context = nil
-
@yield_receiver_to_implementation_block = false
-
-
@implementation = Implementation.new
-
self.inner_implementation_action = implementation_block
-
end
-
# rubocop:enable Style/ParameterLists
-
-
1
def expected_args
-
@argument_list_matcher.expected_args
-
end
-
-
1
def and_yield_receiver_to_implementation
-
@yield_receiver_to_implementation_block = true
-
self
-
end
-
-
1
def yield_receiver_to_implementation_block?
-
@yield_receiver_to_implementation_block
-
end
-
-
1
def matches?(message, *args)
-
@message == message && @argument_list_matcher.args_match?(*args)
-
end
-
-
1
def safe_invoke(parent_stub, *args, &block)
-
invoke_incrementing_actual_calls_by(1, false, parent_stub, *args, &block)
-
end
-
-
1
def invoke(parent_stub, *args, &block)
-
invoke_incrementing_actual_calls_by(1, true, parent_stub, *args, &block)
-
end
-
-
1
def invoke_without_incrementing_received_count(parent_stub, *args, &block)
-
invoke_incrementing_actual_calls_by(0, true, parent_stub, *args, &block)
-
end
-
-
1
def negative?
-
@expected_received_count == 0 && !@at_least
-
end
-
-
1
def called_max_times?
-
@expected_received_count != :any &&
-
!@at_least &&
-
@expected_received_count > 0 &&
-
@actual_received_count >= @expected_received_count
-
end
-
-
1
def matches_name_but_not_args(message, *args)
-
@message == message && !@argument_list_matcher.args_match?(*args)
-
end
-
-
1
def verify_messages_received
-
return if expected_messages_received?
-
generate_error
-
end
-
-
1
def expected_messages_received?
-
ignoring_args? || matches_exact_count? || matches_at_least_count? || matches_at_most_count?
-
end
-
-
1
def ensure_expected_ordering_received!
-
@order_group.verify_invocation_order(self) if @ordered
-
true
-
end
-
-
1
def ignoring_args?
-
@expected_received_count == :any
-
end
-
-
1
def matches_at_least_count?
-
@at_least && @actual_received_count >= @expected_received_count
-
end
-
-
1
def matches_at_most_count?
-
@at_most && @actual_received_count <= @expected_received_count
-
end
-
-
1
def matches_exact_count?
-
@expected_received_count == @actual_received_count
-
end
-
-
1
def similar_messages
-
@similar_messages ||= []
-
end
-
-
1
def advise(*args)
-
similar_messages << args
-
end
-
-
1
def unadvise(args)
-
similar_messages.delete_if { |message| args.include?(message) }
-
end
-
-
1
def generate_error
-
if similar_messages.empty?
-
@error_generator.raise_expectation_error(
-
@message, @expected_received_count, @argument_list_matcher,
-
@actual_received_count, expectation_count_type, expected_args,
-
@expected_from, exception_source_id
-
)
-
else
-
@error_generator.raise_similar_message_args_error(
-
self, @similar_messages, @expected_from
-
)
-
end
-
end
-
-
1
def raise_unexpected_message_args_error(args_for_multiple_calls)
-
@error_generator.raise_unexpected_message_args_error(self, args_for_multiple_calls, exception_source_id)
-
end
-
-
1
def expectation_count_type
-
return :at_least if @at_least
-
return :at_most if @at_most
-
nil
-
end
-
-
1
def description_for(verb)
-
@error_generator.describe_expectation(
-
verb, @message, @expected_received_count,
-
@actual_received_count, expected_args
-
)
-
end
-
-
1
def raise_out_of_order_error
-
@error_generator.raise_out_of_order_error @message
-
end
-
-
1
def additional_expected_calls
-
return 0 if @expectation_type == :stub || !@exactly
-
@expected_received_count - 1
-
end
-
-
1
def ordered?
-
@ordered
-
end
-
-
1
def negative_expectation_for?(message)
-
@message == message && negative?
-
end
-
-
1
def actual_received_count_matters?
-
@at_least || @at_most || @exactly
-
end
-
-
1
def increase_actual_received_count!
-
@actual_received_count += 1
-
end
-
-
1
private
-
-
1
def exception_source_id
-
@exception_source_id ||= "#{self.class.name} #{__id__}"
-
end
-
-
1
def invoke_incrementing_actual_calls_by(increment, allowed_to_fail, parent_stub, *args, &block)
-
args.unshift(orig_object) if yield_receiver_to_implementation_block?
-
-
if negative? || (allowed_to_fail && (@exactly || @at_most) && (@actual_received_count == @expected_received_count))
-
# args are the args we actually received, @argument_list_matcher is the
-
# list of args we were expecting
-
@error_generator.raise_expectation_error(
-
@message, @expected_received_count,
-
@argument_list_matcher,
-
@actual_received_count + increment,
-
expectation_count_type, args, nil, exception_source_id
-
)
-
end
-
-
@order_group.handle_order_constraint self
-
-
if implementation.present?
-
implementation.call(*args, &block)
-
elsif parent_stub
-
parent_stub.invoke(nil, *args, &block)
-
end
-
ensure
-
@actual_received_count += increment
-
end
-
-
1
def has_been_invoked?
-
@actual_received_count > 0
-
end
-
-
1
def raise_already_invoked_error_if_necessary(calling_customization)
-
return unless has_been_invoked?
-
-
error_generator.raise_already_invoked_error(message, calling_customization)
-
end
-
-
1
def set_expected_received_count(relativity, n)
-
@at_least = (relativity == :at_least)
-
@at_most = (relativity == :at_most)
-
@exactly = (relativity == :exactly)
-
@expected_received_count = case n
-
when Numeric then n
-
when :once then 1
-
when :twice then 2
-
when :thrice then 3
-
end
-
end
-
-
1
def initial_implementation_action=(action)
-
implementation.initial_action = action
-
end
-
-
1
def inner_implementation_action=(action)
-
return unless action
-
warn_about_stub_override if implementation.inner_action
-
implementation.inner_action = action
-
end
-
-
1
def terminal_implementation_action=(action)
-
implementation.terminal_action = action
-
end
-
-
1
def warn_about_stub_override
-
RSpec.warning(
-
"You're overriding a previous stub implementation of `#{@message}`. " \
-
"Called from #{CallerFilter.first_non_rspec_line}."
-
)
-
end
-
-
1
def wrap_original(method_name, &block)
-
if RSpec::Mocks::TestDouble === @method_double.object
-
@error_generator.raise_only_valid_on_a_partial_double(method_name)
-
else
-
warn_about_stub_override if implementation.inner_action
-
@implementation = AndWrapOriginalImplementation.new(@method_double.original_implementation_callable, block)
-
@yield_receiver_to_implementation_block = false
-
end
-
-
nil
-
end
-
end
-
-
1
include ImplementationDetails
-
end
-
-
# Handles the implementation of an `and_yield` declaration.
-
# @private
-
1
class AndYieldImplementation
-
1
def initialize(args_to_yield, eval_context, error_generator)
-
@args_to_yield = args_to_yield
-
@eval_context = eval_context
-
@error_generator = error_generator
-
end
-
-
1
def call(*_args_to_ignore, &block)
-
return if @args_to_yield.empty? && @eval_context.nil?
-
-
@error_generator.raise_missing_block_error @args_to_yield unless block
-
value = nil
-
block_signature = Support::BlockSignature.new(block)
-
-
@args_to_yield.each do |args|
-
unless Support::StrictSignatureVerifier.new(block_signature, args).valid?
-
@error_generator.raise_wrong_arity_error(args, block_signature)
-
end
-
-
value = @eval_context ? @eval_context.instance_exec(*args, &block) : block.call(*args)
-
end
-
value
-
end
-
end
-
-
# Handles the implementation of an `and_return` implementation.
-
# @private
-
1
class AndReturnImplementation
-
1
def initialize(values_to_return)
-
@values_to_return = values_to_return
-
end
-
-
1
def call(*_args_to_ignore, &_block)
-
if @values_to_return.size > 1
-
@values_to_return.shift
-
else
-
@values_to_return.first
-
end
-
end
-
end
-
-
# Represents a configured implementation. Takes into account
-
# any number of sub-implementations.
-
# @private
-
1
class Implementation
-
1
attr_accessor :initial_action, :inner_action, :terminal_action
-
-
1
def call(*args, &block)
-
actions.map do |action|
-
action.call(*args, &block)
-
end.last
-
end
-
-
1
def present?
-
actions.any?
-
end
-
-
1
private
-
-
1
def actions
-
[initial_action, inner_action, terminal_action].compact
-
end
-
end
-
-
# Represents an `and_call_original` implementation.
-
# @private
-
1
class AndWrapOriginalImplementation
-
1
def initialize(method, block)
-
@method = method
-
@block = block
-
end
-
-
1
CannotModifyFurtherError = Class.new(StandardError)
-
-
1
def initial_action=(_value)
-
raise cannot_modify_further_error
-
end
-
-
1
def inner_action=(_value)
-
raise cannot_modify_further_error
-
end
-
-
1
def terminal_action=(_value)
-
raise cannot_modify_further_error
-
end
-
-
1
def present?
-
true
-
end
-
-
1
def inner_action
-
true
-
end
-
-
1
def call(*args, &block)
-
@block.call(@method, *args, &block)
-
end
-
-
1
private
-
-
1
def cannot_modify_further_error
-
CannotModifyFurtherError.new "This method has already been configured " \
-
"to call the original implementation, and cannot be modified further."
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class MethodDouble
-
# @private
-
1
attr_reader :method_name, :object, :expectations, :stubs, :method_stasher
-
-
# @private
-
1
def initialize(object, method_name, proxy)
-
@method_name = method_name
-
@object = object
-
@proxy = proxy
-
-
@original_visibility = nil
-
@method_stasher = InstanceMethodStasher.new(object, method_name)
-
@method_is_proxied = false
-
@expectations = []
-
@stubs = []
-
end
-
-
1
def original_implementation_callable
-
# If original method is not present, uses the `method_missing`
-
# handler of the object. This accounts for cases where the user has not
-
# correctly defined `respond_to?`, and also 1.8 which does not provide
-
# method handles for missing methods even if `respond_to?` is correct.
-
@original_implementation_callable ||= original_method ||
-
Proc.new do |*args, &block|
-
@object.__send__(:method_missing, @method_name, *args, &block)
-
end
-
end
-
-
1
alias_method :save_original_implementation_callable!, :original_implementation_callable
-
-
1
def original_method
-
@original_method ||=
-
@method_stasher.original_method ||
-
@proxy.original_method_handle_for(method_name)
-
end
-
-
# @private
-
1
def visibility
-
@proxy.visibility_for(@method_name)
-
end
-
-
# @private
-
1
def object_singleton_class
-
class << @object; self; end
-
end
-
-
# @private
-
1
def configure_method
-
@original_visibility = visibility
-
@method_stasher.stash unless @method_is_proxied
-
define_proxy_method
-
end
-
-
# @private
-
1
def define_proxy_method
-
return if @method_is_proxied
-
-
save_original_implementation_callable!
-
definition_target.class_exec(self, method_name, visibility) do |method_double, method_name, visibility|
-
define_method(method_name) do |*args, &block|
-
method_double.proxy_method_invoked(self, *args, &block)
-
end
-
__send__(visibility, method_name)
-
end
-
-
@method_is_proxied = true
-
end
-
-
# The implementation of the proxied method. Subclasses may override this
-
# method to perform additional operations.
-
#
-
# @private
-
1
def proxy_method_invoked(_obj, *args, &block)
-
@proxy.message_received method_name, *args, &block
-
end
-
-
# @private
-
1
def restore_original_method
-
return show_frozen_warning if object_singleton_class.frozen?
-
return unless @method_is_proxied
-
-
remove_method_from_definition_target
-
@method_stasher.restore if @method_stasher.method_is_stashed?
-
restore_original_visibility
-
-
@method_is_proxied = false
-
end
-
-
# @private
-
1
def show_frozen_warning
-
RSpec.warn_with(
-
"WARNING: rspec-mocks was unable to restore the original `#{@method_name}` " \
-
"method on #{@object.inspect} because it has been frozen. If you reuse this " \
-
"object, `#{@method_name}` will continue to respond with its stub implementation.",
-
:call_site => nil,
-
:use_spec_location_as_call_site => true
-
)
-
end
-
-
# @private
-
1
def restore_original_visibility
-
return unless @original_visibility &&
-
MethodReference.method_defined_at_any_visibility?(object_singleton_class, @method_name)
-
-
object_singleton_class.__send__(@original_visibility, method_name)
-
end
-
-
# @private
-
1
def verify
-
expectations.each { |e| e.verify_messages_received }
-
end
-
-
# @private
-
1
def reset
-
restore_original_method
-
clear
-
end
-
-
# @private
-
1
def clear
-
expectations.clear
-
stubs.clear
-
end
-
-
# The type of message expectation to create has been extracted to its own
-
# method so that subclasses can override it.
-
#
-
# @private
-
1
def message_expectation_class
-
MessageExpectation
-
end
-
-
# @private
-
1
def add_expectation(error_generator, expectation_ordering, expected_from, opts, &implementation)
-
configure_method
-
expectation = message_expectation_class.new(error_generator, expectation_ordering,
-
expected_from, self, :expectation, opts, &implementation)
-
expectations << expectation
-
expectation
-
end
-
-
# @private
-
1
def build_expectation(error_generator, expectation_ordering)
-
expected_from = IGNORED_BACKTRACE_LINE
-
message_expectation_class.new(error_generator, expectation_ordering, expected_from, self)
-
end
-
-
# @private
-
1
def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &implementation)
-
configure_method
-
stub = message_expectation_class.new(error_generator, expectation_ordering, expected_from,
-
self, :stub, opts, &implementation)
-
stubs.unshift stub
-
stub
-
end
-
-
# A simple stub can only return a concrete value for a message, and
-
# cannot match on arguments. It is used as an optimization over
-
# `add_stub` / `add_expectation` where it is known in advance that this
-
# is all that will be required of a stub, such as when passing attributes
-
# to the `double` example method. They do not stash or restore existing method
-
# definitions.
-
#
-
# @private
-
1
def add_simple_stub(method_name, response)
-
setup_simple_method_double method_name, response, stubs
-
end
-
-
# @private
-
1
def add_simple_expectation(method_name, response, error_generator, backtrace_line)
-
setup_simple_method_double method_name, response, expectations, error_generator, backtrace_line
-
end
-
-
# @private
-
1
def setup_simple_method_double(method_name, response, collection, error_generator=nil, backtrace_line=nil)
-
define_proxy_method
-
-
me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
-
collection.unshift me
-
me
-
end
-
-
# @private
-
1
def add_default_stub(*args, &implementation)
-
return if stubs.any?
-
add_stub(*args, &implementation)
-
end
-
-
# @private
-
1
def remove_stub
-
raise_method_not_stubbed_error if stubs.empty?
-
remove_stub_if_present
-
end
-
-
# @private
-
1
def remove_stub_if_present
-
expectations.empty? ? reset : stubs.clear
-
end
-
-
# @private
-
1
def raise_method_not_stubbed_error
-
RSpec::Mocks.error_generator.raise_method_not_stubbed_error(method_name)
-
end
-
-
# In Ruby 2.0.0 and above prepend will alter the method lookup chain.
-
# We use an object's singleton class to define method doubles upon,
-
# however if the object has had it's singleton class (as opposed to
-
# it's actual class) prepended too then the the method lookup chain
-
# will look in the prepended module first, **before** the singleton
-
# class.
-
#
-
# This code works around that by providing a mock definition target
-
# that is either the singleton class, or if necessary, a prepended module
-
# of our own.
-
#
-
1
if Support::RubyFeatures.module_prepends_supported?
-
-
1
private
-
-
# We subclass `Module` in order to be able to easily detect our prepended module.
-
1
RSpecPrependedModule = Class.new(Module)
-
-
1
def definition_target
-
@definition_target ||= usable_rspec_prepended_module || object_singleton_class
-
end
-
-
1
def usable_rspec_prepended_module
-
@proxy.prepended_modules_of_singleton_class.each do |mod|
-
# If we have one of our modules prepended before one of the user's
-
# modules that defines the method, use that, since our module's
-
# definition will take precedence.
-
return mod if RSpecPrependedModule === mod
-
-
# If we hit a user module with the method defined first,
-
# we must create a new prepend module, even if one exists later,
-
# because ours will only take precedence if it comes first.
-
return new_rspec_prepended_module if mod.method_defined?(method_name)
-
end
-
-
nil
-
end
-
-
1
def new_rspec_prepended_module
-
RSpecPrependedModule.new.tap do |mod|
-
object_singleton_class.__send__ :prepend, mod
-
end
-
end
-
-
else
-
-
private
-
-
def definition_target
-
object_singleton_class
-
end
-
-
end
-
-
1
private
-
-
1
def remove_method_from_definition_target
-
definition_target.__send__(:remove_method, @method_name)
-
rescue NameError
-
# This can happen when the method has been monkeyed with by
-
# something outside RSpec. This happens, for example, when
-
# `file.write` has been stubbed, and then `file.reopen(other_io)`
-
# is later called, as `File#reopen` appears to redefine `write`.
-
#
-
# Note: we could avoid rescuing this by checking
-
# `definition_target.instance_method(@method_name).owner == definition_target`,
-
# saving us from the cost of the expensive exception, but this error is
-
# extremely rare (it was discovered on 2014-12-30, only happens on
-
# RUBY_VERSION < 2.0 and our spec suite only hits this condition once),
-
# so we'd rather avoid the cost of that check for every method double,
-
# and risk the rare situation where this exception will get raised.
-
RSpec.warn_with(
-
"WARNING: RSpec could not fully restore #{@object.inspect}." \
-
"#{@method_name}, possibly because the method has been redefined " \
-
"by something outside of RSpec."
-
)
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support 'comparable_version'
-
-
1
module RSpec
-
1
module Mocks
-
# Represents a method on an object that may or may not be defined.
-
# The method may be an instance method on a module or a method on
-
# any object.
-
#
-
# @private
-
1
class MethodReference
-
1
def self.for(object_reference, method_name)
-
new(object_reference, method_name)
-
end
-
-
1
def initialize(object_reference, method_name)
-
@object_reference = object_reference
-
@method_name = method_name
-
end
-
-
# A method is implemented if sending the message does not result in
-
# a `NoMethodError`. It might be dynamically implemented by
-
# `method_missing`.
-
1
def implemented?
-
@object_reference.when_loaded do |m|
-
method_implemented?(m)
-
end
-
end
-
-
# Returns true if we definitively know that sending the method
-
# will result in a `NoMethodError`.
-
#
-
# This is not simply the inverse of `implemented?`: there are
-
# cases when we don't know if a method is implemented and
-
# both `implemented?` and `unimplemented?` will return false.
-
1
def unimplemented?
-
@object_reference.when_loaded do |_m|
-
return !implemented?
-
end
-
-
# If it's not loaded, then it may be implemented but we can't check.
-
false
-
end
-
-
# A method is defined if we are able to get a `Method` object for it.
-
# In that case, we can assert against metadata like the arity.
-
1
def defined?
-
@object_reference.when_loaded do |m|
-
method_defined?(m)
-
end
-
end
-
-
1
def with_signature
-
return unless (original = original_method)
-
yield Support::MethodSignature.new(original)
-
end
-
-
1
def visibility
-
@object_reference.when_loaded do |m|
-
return visibility_from(m)
-
end
-
-
# When it's not loaded, assume it's public. We don't want to
-
# wrongly treat the method as private.
-
:public
-
end
-
-
1
private
-
-
1
def original_method
-
@object_reference.when_loaded do |m|
-
self.defined? && find_method(m)
-
end
-
end
-
-
1
def self.instance_method_visibility_for(klass, method_name)
-
if klass.public_method_defined?(method_name)
-
:public
-
elsif klass.private_method_defined?(method_name)
-
:private
-
elsif klass.protected_method_defined?(method_name)
-
:protected
-
end
-
end
-
-
1
class << self
-
1
alias method_defined_at_any_visibility? instance_method_visibility_for
-
end
-
-
1
def self.method_visibility_for(object, method_name)
-
vis = instance_method_visibility_for(class << object; self; end, method_name)
-
-
# If the method is not defined on the class, `instance_method_visibility_for`
-
# returns `nil`. However, it may be handled dynamically by `method_missing`,
-
# so here we check `respond_to` (passing false to not check private methods).
-
#
-
# This only considers the public case, but I don't think it's possible to
-
# write `method_missing` in such a way that it handles a dynamic message
-
# with private or protected visibility. Ruby doesn't provide you with
-
# the caller info.
-
return vis unless vis.nil?
-
-
proxy = RSpec::Mocks.space.proxy_for(object)
-
respond_to = proxy.method_double_if_exists_for_message(:respond_to?)
-
-
visible = respond_to && respond_to.original_method.call(method_name) ||
-
object.respond_to?(method_name)
-
-
return :public if visible
-
end
-
end
-
-
# @private
-
1
class InstanceMethodReference < MethodReference
-
1
private
-
-
1
def method_implemented?(mod)
-
MethodReference.method_defined_at_any_visibility?(mod, @method_name)
-
end
-
-
# Ideally, we'd use `respond_to?` for `method_implemented?` but we need a
-
# reference to an instance to do that and we don't have one. Note that
-
# we may get false negatives: if the method is implemented via
-
# `method_missing`, we'll return `false` even though it meets our
-
# definition of "implemented". However, it's the best we can do.
-
1
alias method_defined? method_implemented?
-
-
# works around the fact that repeated calls for method parameters will
-
# falsely return empty arrays on JRuby in certain circumstances, this
-
# is necessary here because we can't dup/clone UnboundMethods.
-
#
-
# This is necessary due to a bug in JRuby prior to 1.7.5 fixed in:
-
# https://github.com/jruby/jruby/commit/99a0613fe29935150d76a9a1ee4cf2b4f63f4a27
-
1
if RUBY_PLATFORM == 'java' && RSpec::Support::ComparableVersion.new(JRUBY_VERSION) < '1.7.5'
-
def find_method(mod)
-
mod.dup.instance_method(@method_name)
-
end
-
else
-
1
def find_method(mod)
-
mod.instance_method(@method_name)
-
end
-
end
-
-
1
def visibility_from(mod)
-
MethodReference.instance_method_visibility_for(mod, @method_name)
-
end
-
end
-
-
# @private
-
1
class ObjectMethodReference < MethodReference
-
1
def self.for(object_reference, method_name)
-
if ClassNewMethodReference.applies_to?(method_name) { object_reference.when_loaded { |o| o } }
-
ClassNewMethodReference.new(object_reference, method_name)
-
else
-
super
-
end
-
end
-
-
1
private
-
-
1
def method_implemented?(object)
-
object.respond_to?(@method_name, true)
-
end
-
-
1
def method_defined?(object)
-
(class << object; self; end).method_defined?(@method_name)
-
end
-
-
1
def find_method(object)
-
object.method(@method_name)
-
end
-
-
1
def visibility_from(object)
-
MethodReference.method_visibility_for(object, @method_name)
-
end
-
end
-
-
# When a class's `.new` method is stubbed, we want to use the method
-
# signature from `#initialize` because `.new`'s signature is a generic
-
# `def new(*args)` and it simply delegates to `#initialize` and forwards
-
# all args...so the method with the actually used signature is `#initialize`.
-
#
-
# This method reference implementation handles that specific case.
-
# @private
-
1
class ClassNewMethodReference < ObjectMethodReference
-
1
def self.applies_to?(method_name)
-
return false unless method_name == :new
-
klass = yield
-
return false unless klass.respond_to?(:new, true)
-
-
# We only want to apply our special logic to normal `new` methods.
-
# Methods that the user has monkeyed with should be left as-is.
-
klass.method(:new).owner == ::Class
-
end
-
-
1
def with_signature
-
@object_reference.when_loaded do |klass|
-
yield Support::MethodSignature.new(klass.instance_method(:initialize))
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support 'recursive_const_methods'
-
-
1
module RSpec
-
1
module Mocks
-
# Provides information about constants that may (or may not)
-
# have been mutated by rspec-mocks.
-
1
class Constant
-
1
extend Support::RecursiveConstMethods
-
-
# @api private
-
1
def initialize(name)
-
@name = name
-
@previously_defined = false
-
@stubbed = false
-
@hidden = false
-
@valid_name = true
-
yield self if block_given?
-
end
-
-
# @return [String] The fully qualified name of the constant.
-
1
attr_reader :name
-
-
# @return [Object, nil] The original value (e.g. before it
-
# was mutated by rspec-mocks) of the constant, or
-
# nil if the constant was not previously defined.
-
1
attr_accessor :original_value
-
-
# @private
-
1
attr_writer :previously_defined, :stubbed, :hidden, :valid_name
-
-
# @return [Boolean] Whether or not the constant was defined
-
# before the current example.
-
1
def previously_defined?
-
@previously_defined
-
end
-
-
# @return [Boolean] Whether or not rspec-mocks has mutated
-
# (stubbed or hidden) this constant.
-
1
def mutated?
-
@stubbed || @hidden
-
end
-
-
# @return [Boolean] Whether or not rspec-mocks has stubbed
-
# this constant.
-
1
def stubbed?
-
@stubbed
-
end
-
-
# @return [Boolean] Whether or not rspec-mocks has hidden
-
# this constant.
-
1
def hidden?
-
@hidden
-
end
-
-
# @return [Boolean] Whether or not the provided constant name
-
# is a valid Ruby constant name.
-
1
def valid_name?
-
@valid_name
-
end
-
-
# The default `to_s` isn't very useful, so a custom version is provided.
-
1
def to_s
-
"#<#{self.class.name} #{name}>"
-
end
-
1
alias inspect to_s
-
-
# @private
-
1
def self.unmutated(name)
-
previously_defined = recursive_const_defined?(name)
-
rescue NameError
-
new(name) do |c|
-
c.valid_name = false
-
end
-
else
-
new(name) do |const|
-
const.previously_defined = previously_defined
-
const.original_value = recursive_const_get(name) if previously_defined
-
end
-
end
-
-
# Queries rspec-mocks to find out information about the named constant.
-
#
-
# @param [String] name the name of the constant
-
# @return [Constant] an object contaning information about the named
-
# constant.
-
1
def self.original(name)
-
mutator = ::RSpec::Mocks.space.constant_mutator_for(name)
-
mutator ? mutator.to_constant : unmutated(name)
-
end
-
end
-
-
# Provides a means to stub constants.
-
1
class ConstantMutator
-
1
extend Support::RecursiveConstMethods
-
-
# Stubs a constant.
-
#
-
# @param (see ExampleMethods#stub_const)
-
# @option (see ExampleMethods#stub_const)
-
# @return (see ExampleMethods#stub_const)
-
#
-
# @see ExampleMethods#stub_const
-
# @note It's recommended that you use `stub_const` in your
-
# examples. This is an alternate public API that is provided
-
# so you can stub constants in other contexts (e.g. helper
-
# classes).
-
1
def self.stub(constant_name, value, options={})
-
mutator = if recursive_const_defined?(constant_name, &raise_on_invalid_const)
-
DefinedConstantReplacer
-
else
-
UndefinedConstantSetter
-
end
-
-
mutate(mutator.new(constant_name, value, options[:transfer_nested_constants]))
-
value
-
end
-
-
# Hides a constant.
-
#
-
# @param (see ExampleMethods#hide_const)
-
#
-
# @see ExampleMethods#hide_const
-
# @note It's recommended that you use `hide_const` in your
-
# examples. This is an alternate public API that is provided
-
# so you can hide constants in other contexts (e.g. helper
-
# classes).
-
1
def self.hide(constant_name)
-
mutate(ConstantHider.new(constant_name, nil, {}))
-
nil
-
end
-
-
# Contains common functionality used by all of the constant mutators.
-
#
-
# @private
-
1
class BaseMutator
-
1
include Support::RecursiveConstMethods
-
-
1
attr_reader :original_value, :full_constant_name
-
-
1
def initialize(full_constant_name, mutated_value, transfer_nested_constants)
-
@full_constant_name = normalize_const_name(full_constant_name)
-
@mutated_value = mutated_value
-
@transfer_nested_constants = transfer_nested_constants
-
@context_parts = @full_constant_name.split('::')
-
@const_name = @context_parts.pop
-
@reset_performed = false
-
end
-
-
1
def to_constant
-
const = Constant.new(full_constant_name)
-
const.original_value = original_value
-
-
const
-
end
-
-
1
def idempotently_reset
-
reset unless @reset_performed
-
@reset_performed = true
-
end
-
end
-
-
# Hides a defined constant for the duration of an example.
-
#
-
# @private
-
1
class ConstantHider < BaseMutator
-
1
def mutate
-
return unless (@defined = recursive_const_defined?(full_constant_name))
-
@context = recursive_const_get(@context_parts.join('::'))
-
@original_value = get_const_defined_on(@context, @const_name)
-
-
@context.__send__(:remove_const, @const_name)
-
end
-
-
1
def to_constant
-
return Constant.unmutated(full_constant_name) unless @defined
-
-
const = super
-
const.hidden = true
-
const.previously_defined = true
-
-
const
-
end
-
-
1
def reset
-
return unless @defined
-
@context.const_set(@const_name, @original_value)
-
end
-
end
-
-
# Replaces a defined constant for the duration of an example.
-
#
-
# @private
-
1
class DefinedConstantReplacer < BaseMutator
-
1
def initialize(*args)
-
super
-
@constants_to_transfer = []
-
end
-
-
1
def mutate
-
@context = recursive_const_get(@context_parts.join('::'))
-
@original_value = get_const_defined_on(@context, @const_name)
-
-
@constants_to_transfer = verify_constants_to_transfer!
-
-
@context.__send__(:remove_const, @const_name)
-
@context.const_set(@const_name, @mutated_value)
-
-
transfer_nested_constants
-
end
-
-
1
def to_constant
-
const = super
-
const.stubbed = true
-
const.previously_defined = true
-
-
const
-
end
-
-
1
def reset
-
@constants_to_transfer.each do |const|
-
@mutated_value.__send__(:remove_const, const)
-
end
-
-
@context.__send__(:remove_const, @const_name)
-
@context.const_set(@const_name, @original_value)
-
end
-
-
1
def transfer_nested_constants
-
@constants_to_transfer.each do |const|
-
@mutated_value.const_set(const, get_const_defined_on(original_value, const))
-
end
-
end
-
-
1
def verify_constants_to_transfer!
-
return [] unless should_transfer_nested_constants?
-
-
{ @original_value => "the original value", @mutated_value => "the stubbed value" }.each do |value, description|
-
next if value.respond_to?(:constants)
-
-
raise ArgumentError,
-
"Cannot transfer nested constants for #{@full_constant_name} " \
-
"since #{description} is not a class or module and only classes " \
-
"and modules support nested constants."
-
end
-
-
if Array === @transfer_nested_constants
-
@transfer_nested_constants = @transfer_nested_constants.map(&:to_s) if RUBY_VERSION == '1.8.7'
-
undefined_constants = @transfer_nested_constants - constants_defined_on(@original_value)
-
-
if undefined_constants.any?
-
available_constants = constants_defined_on(@original_value) - @transfer_nested_constants
-
raise ArgumentError,
-
"Cannot transfer nested constant(s) #{undefined_constants.join(' and ')} " \
-
"for #{@full_constant_name} since they are not defined. Did you mean " \
-
"#{available_constants.join(' or ')}?"
-
end
-
-
@transfer_nested_constants
-
else
-
constants_defined_on(@original_value)
-
end
-
end
-
-
1
def should_transfer_nested_constants?
-
return true if @transfer_nested_constants
-
return false unless RSpec::Mocks.configuration.transfer_nested_constants?
-
@original_value.respond_to?(:constants) && @mutated_value.respond_to?(:constants)
-
end
-
end
-
-
# Sets an undefined constant for the duration of an example.
-
#
-
# @private
-
1
class UndefinedConstantSetter < BaseMutator
-
1
def mutate
-
@parent = @context_parts.inject(Object) do |klass, name|
-
if const_defined_on?(klass, name)
-
get_const_defined_on(klass, name)
-
else
-
ConstantMutator.stub(name_for(klass, name), Module.new)
-
end
-
end
-
-
@parent.const_set(@const_name, @mutated_value)
-
end
-
-
1
def to_constant
-
const = super
-
const.stubbed = true
-
const.previously_defined = false
-
-
const
-
end
-
-
1
def reset
-
@parent.__send__(:remove_const, @const_name)
-
end
-
-
1
private
-
-
1
def name_for(parent, name)
-
root = if parent == Object
-
''
-
else
-
parent.name
-
end
-
root + '::' + name
-
end
-
end
-
-
# Uses the mutator to mutate (stub or hide) a constant. Ensures that
-
# the mutator is correctly registered so it can be backed out at the end
-
# of the test.
-
#
-
# @private
-
1
def self.mutate(mutator)
-
::RSpec::Mocks.space.register_constant_mutator(mutator)
-
mutator.mutate
-
end
-
-
# Used internally by the constant stubbing to raise a helpful
-
# error when a constant like "A::B::C" is stubbed and A::B is
-
# not a module (and thus, it's impossible to define "A::B::C"
-
# since only modules can have nested constants).
-
#
-
# @api private
-
1
def self.raise_on_invalid_const
-
lambda do |const_name, failed_name|
-
raise "Cannot stub constant #{failed_name} on #{const_name} " \
-
"since #{const_name} is not a module."
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class ObjectReference
-
# Returns an appropriate Object or Module reference based
-
# on the given argument.
-
1
def self.for(object_module_or_name, allow_direct_object_refs=false)
-
case object_module_or_name
-
when Module
-
if anonymous_module?(object_module_or_name)
-
DirectObjectReference.new(object_module_or_name)
-
else
-
# Use a `NamedObjectReference` if it has a name because this
-
# will use the original value of the constant in case it has
-
# been stubbed.
-
NamedObjectReference.new(name_of(object_module_or_name))
-
end
-
when String
-
NamedObjectReference.new(object_module_or_name)
-
else
-
if allow_direct_object_refs
-
DirectObjectReference.new(object_module_or_name)
-
else
-
raise ArgumentError,
-
"Module or String expected, got #{object_module_or_name.inspect}"
-
end
-
end
-
end
-
-
1
if Module.new.name.nil?
-
1
def self.anonymous_module?(mod)
-
!name_of(mod)
-
end
-
else # 1.8.7
-
def self.anonymous_module?(mod)
-
name_of(mod) == ""
-
end
-
end
-
1
private_class_method :anonymous_module?
-
-
1
def self.name_of(mod)
-
MODULE_NAME_METHOD.bind(mod).call
-
end
-
1
private_class_method :name_of
-
-
# @private
-
1
MODULE_NAME_METHOD = Module.instance_method(:name)
-
end
-
-
# An implementation of rspec-mocks' reference interface.
-
# Used when an object is passed to {ExampleMethods#object_double}, or
-
# an anonymous class or module is passed to {ExampleMethods#instance_double}
-
# or {ExampleMethods#class_double}.
-
# Represents a reference to that object.
-
# @see NamedObjectReference
-
1
class DirectObjectReference
-
# @param object [Object] the object to which this refers
-
1
def initialize(object)
-
@object = object
-
end
-
-
# @return [String] the object's description (via `#inspect`).
-
1
def description
-
@object.inspect
-
end
-
-
# Defined for interface parity with the other object reference
-
# implementations. Raises an `ArgumentError` to indicate that `as_stubbed_const`
-
# is invalid when passing an object argument to `object_double`.
-
1
def const_to_replace
-
raise ArgumentError,
-
"Can not perform constant replacement with an anonymous object."
-
end
-
-
# The target of the verifying double (the object itself).
-
#
-
# @return [Object]
-
1
def target
-
@object
-
end
-
-
# Always returns true for an object as the class is defined.
-
#
-
# @return [true]
-
1
def defined?
-
true
-
end
-
-
# Yields if the reference target is loaded, providing a generic mechanism
-
# to optionally run a bit of code only when a reference's target is
-
# loaded.
-
#
-
# This specific implementation always yields because direct references
-
# are always loaded.
-
#
-
# @yield [Object] the target of this reference.
-
1
def when_loaded
-
yield @object
-
end
-
end
-
-
# An implementation of rspec-mocks' reference interface.
-
# Used when a string is passed to {ExampleMethods#object_double},
-
# and when a string, named class or named module is passed to
-
# {ExampleMethods#instance_double}, or {ExampleMethods#class_double}.
-
# Represents a reference to the object named (via a constant lookup)
-
# by the string.
-
# @see DirectObjectReference
-
1
class NamedObjectReference
-
# @param const_name [String] constant name
-
1
def initialize(const_name)
-
@const_name = const_name
-
end
-
-
# @return [Boolean] true if the named constant is defined, false otherwise.
-
1
def defined?
-
!!object
-
end
-
-
# @return [String] the constant name to replace with a double.
-
1
def const_to_replace
-
@const_name
-
end
-
1
alias description const_to_replace
-
-
# @return [Object, nil] the target of the verifying double (the named object), or
-
# nil if it is not defined.
-
1
def target
-
object
-
end
-
-
# Yields if the reference target is loaded, providing a generic mechanism
-
# to optionally run a bit of code only when a reference's target is
-
# loaded.
-
#
-
# @yield [Object] the target object
-
1
def when_loaded
-
yield object if object
-
end
-
-
1
private
-
-
1
def object
-
return @object if defined?(@object)
-
@object = Constant.original(@const_name).original_value
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class OrderGroup
-
1
def initialize
-
14
@expectations = []
-
14
@invocation_order = []
-
14
@index = 0
-
end
-
-
# @private
-
1
def register(expectation)
-
@expectations << expectation
-
end
-
-
1
def invoked(message)
-
@invocation_order << message
-
end
-
-
# @private
-
1
def ready_for?(expectation)
-
remaining_expectations.find(&:ordered?) == expectation
-
end
-
-
# @private
-
1
def consume
-
remaining_expectations.each_with_index do |expectation, index|
-
next unless expectation.ordered?
-
-
@index += index + 1
-
return expectation
-
end
-
nil
-
end
-
-
# @private
-
1
def handle_order_constraint(expectation)
-
return unless expectation.ordered? && remaining_expectations.include?(expectation)
-
return consume if ready_for?(expectation)
-
expectation.raise_out_of_order_error
-
end
-
-
1
def verify_invocation_order(expectation)
-
expectation.raise_out_of_order_error unless expectations_invoked_in_order?
-
true
-
end
-
-
1
def clear
-
@index = 0
-
@invocation_order.clear
-
@expectations.clear
-
end
-
-
1
def empty?
-
@expectations.empty?
-
end
-
-
1
private
-
-
1
def remaining_expectations
-
@expectations[@index..-1] || []
-
end
-
-
1
def expectations_invoked_in_order?
-
invoked_expectations == expected_invocations
-
end
-
-
1
def invoked_expectations
-
@expectations.select { |e| e.ordered? && @invocation_order.include?(e) }
-
end
-
-
1
def expected_invocations
-
@invocation_order.map { |invocation| expectation_for(invocation) }.compact
-
end
-
-
1
def expectation_for(message)
-
@expectations.find { |e| message == e }
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class Proxy
-
1
SpecificMessage = Struct.new(:object, :message, :args) do
-
1
def ==(expectation)
-
expectation.orig_object == object && expectation.matches?(message, *args)
-
end
-
end
-
-
# @private
-
1
def ensure_implemented(*_args)
-
# noop for basic proxies, see VerifyingProxy for behaviour.
-
end
-
-
# @private
-
1
def initialize(object, order_group, options={})
-
@object = object
-
@order_group = order_group
-
@error_generator = ErrorGenerator.new(object)
-
@messages_received = []
-
@options = options
-
@null_object = false
-
@method_doubles = Hash.new { |h, k| h[k] = MethodDouble.new(@object, k, self) }
-
end
-
-
# @private
-
1
attr_reader :object
-
-
# @private
-
1
def null_object?
-
@null_object
-
end
-
-
# @private
-
# Tells the object to ignore any messages that aren't explicitly set as
-
# stubs or message expectations.
-
1
def as_null_object
-
@null_object = true
-
@object
-
end
-
-
# @private
-
1
def original_method_handle_for(_message)
-
nil
-
end
-
-
1
DEFAULT_MESSAGE_EXPECTATION_OPTS = {}.freeze
-
-
# @private
-
1
def add_message_expectation(method_name, opts=DEFAULT_MESSAGE_EXPECTATION_OPTS, &block)
-
location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line }
-
meth_double = method_double_for(method_name)
-
-
if null_object? && !block
-
meth_double.add_default_stub(@error_generator, @order_group, location, opts) do
-
@object
-
end
-
end
-
-
meth_double.add_expectation @error_generator, @order_group, location, opts, &block
-
end
-
-
# @private
-
1
def add_simple_expectation(method_name, response, location)
-
method_double_for(method_name).add_simple_expectation method_name, response, @error_generator, location
-
end
-
-
# @private
-
1
def build_expectation(method_name)
-
meth_double = method_double_for(method_name)
-
-
meth_double.build_expectation(
-
@error_generator,
-
@order_group
-
)
-
end
-
-
# @private
-
1
def replay_received_message_on(expectation, &block)
-
expected_method_name = expectation.message
-
meth_double = method_double_for(expected_method_name)
-
-
if meth_double.expectations.any?
-
@error_generator.raise_expectation_on_mocked_method(expected_method_name)
-
end
-
-
unless null_object? || meth_double.stubs.any?
-
@error_generator.raise_expectation_on_unstubbed_method(expected_method_name)
-
end
-
-
@messages_received.each do |(actual_method_name, args, received_block)|
-
next unless expectation.matches?(actual_method_name, *args)
-
-
expectation.safe_invoke(nil)
-
block.call(*args, &received_block) if block
-
end
-
end
-
-
# @private
-
1
def check_for_unexpected_arguments(expectation)
-
return if @messages_received.empty?
-
-
return if @messages_received.any? { |method_name, args, _| expectation.matches?(method_name, *args) }
-
-
name_but_not_args, others = @messages_received.partition do |(method_name, args, _)|
-
expectation.matches_name_but_not_args(method_name, *args)
-
end
-
-
return if name_but_not_args.empty? && !others.empty?
-
-
expectation.raise_unexpected_message_args_error(name_but_not_args.map { |args| args[1] })
-
end
-
-
# @private
-
1
def add_stub(method_name, opts={}, &implementation)
-
location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line }
-
method_double_for(method_name).add_stub @error_generator, @order_group, location, opts, &implementation
-
end
-
-
# @private
-
1
def add_simple_stub(method_name, response)
-
method_double_for(method_name).add_simple_stub method_name, response
-
end
-
-
# @private
-
1
def remove_stub(method_name)
-
method_double_for(method_name).remove_stub
-
end
-
-
# @private
-
1
def remove_stub_if_present(method_name)
-
method_double_for(method_name).remove_stub_if_present
-
end
-
-
# @private
-
1
def verify
-
@method_doubles.each_value { |d| d.verify }
-
end
-
-
# @private
-
1
def reset
-
@messages_received.clear
-
end
-
-
# @private
-
1
def received_message?(method_name, *args, &block)
-
@messages_received.any? { |array| array == [method_name, args, block] }
-
end
-
-
# @private
-
1
def messages_arg_list
-
@messages_received.map { |_, args, _| args }
-
end
-
-
# @private
-
1
def has_negative_expectation?(message)
-
method_double_for(message).expectations.find { |expectation| expectation.negative_expectation_for?(message) }
-
end
-
-
# @private
-
1
def record_message_received(message, *args, &block)
-
@order_group.invoked SpecificMessage.new(object, message, args)
-
@messages_received << [message, args, block]
-
end
-
-
# @private
-
1
def message_received(message, *args, &block)
-
record_message_received message, *args, &block
-
-
expectation = find_matching_expectation(message, *args)
-
stub = find_matching_method_stub(message, *args)
-
-
if (stub && expectation && expectation.called_max_times?) || (stub && !expectation)
-
expectation.increase_actual_received_count! if expectation && expectation.actual_received_count_matters?
-
if (expectation = find_almost_matching_expectation(message, *args))
-
expectation.advise(*args) unless expectation.expected_messages_received?
-
end
-
stub.invoke(nil, *args, &block)
-
elsif expectation
-
expectation.unadvise(messages_arg_list)
-
expectation.invoke(stub, *args, &block)
-
elsif (expectation = find_almost_matching_expectation(message, *args))
-
expectation.advise(*args) if null_object? unless expectation.expected_messages_received?
-
-
if null_object? || !has_negative_expectation?(message)
-
expectation.raise_unexpected_message_args_error([args])
-
end
-
elsif (stub = find_almost_matching_stub(message, *args))
-
stub.advise(*args)
-
raise_missing_default_stub_error(stub, [args])
-
elsif Class === @object
-
@object.superclass.__send__(message, *args, &block)
-
else
-
@object.__send__(:method_missing, message, *args, &block)
-
end
-
end
-
-
# @private
-
1
def raise_unexpected_message_error(method_name, args)
-
@error_generator.raise_unexpected_message_error method_name, args
-
end
-
-
# @private
-
1
def raise_missing_default_stub_error(expectation, args_for_multiple_calls)
-
@error_generator.raise_missing_default_stub_error(expectation, args_for_multiple_calls)
-
end
-
-
# @private
-
1
def visibility_for(_method_name)
-
# This is the default (for test doubles). Subclasses override this.
-
:public
-
end
-
-
1
if Support::RubyFeatures.module_prepends_supported?
-
1
def self.prepended_modules_of(klass)
-
ancestors = klass.ancestors
-
-
# `|| 0` is necessary for Ruby 2.0, where the singleton class
-
# is only in the ancestor list when there are prepended modules.
-
singleton_index = ancestors.index(klass) || 0
-
-
ancestors[0, singleton_index]
-
end
-
-
1
def prepended_modules_of_singleton_class
-
@prepended_modules_of_singleton_class ||= RSpec::Mocks::Proxy.prepended_modules_of(@object.singleton_class)
-
end
-
end
-
-
# @private
-
1
def method_double_if_exists_for_message(message)
-
method_double_for(message) if @method_doubles.key?(message.to_sym)
-
end
-
-
1
private
-
-
1
def method_double_for(message)
-
@method_doubles[message.to_sym]
-
end
-
-
1
def find_matching_expectation(method_name, *args)
-
find_best_matching_expectation_for(method_name) do |expectation|
-
expectation.matches?(method_name, *args)
-
end
-
end
-
-
1
def find_almost_matching_expectation(method_name, *args)
-
find_best_matching_expectation_for(method_name) do |expectation|
-
expectation.matches_name_but_not_args(method_name, *args)
-
end
-
end
-
-
1
def find_best_matching_expectation_for(method_name)
-
first_match = nil
-
-
method_double_for(method_name).expectations.each do |expectation|
-
next unless yield expectation
-
return expectation unless expectation.called_max_times?
-
first_match ||= expectation
-
end
-
-
first_match
-
end
-
-
1
def find_matching_method_stub(method_name, *args)
-
method_double_for(method_name).stubs.find { |stub| stub.matches?(method_name, *args) }
-
end
-
-
1
def find_almost_matching_stub(method_name, *args)
-
method_double_for(method_name).stubs.find { |stub| stub.matches_name_but_not_args(method_name, *args) }
-
end
-
end
-
-
# @private
-
1
class TestDoubleProxy < Proxy
-
1
def reset
-
@method_doubles.clear
-
object.__disallow_further_usage!
-
super
-
end
-
end
-
-
# @private
-
1
class PartialDoubleProxy < Proxy
-
1
def original_method_handle_for(message)
-
if any_instance_class_recorder_observing_method?(@object.class, message)
-
message = ::RSpec::Mocks.space.
-
any_instance_recorder_for(@object.class).
-
build_alias_method_name(message)
-
end
-
-
::RSpec::Support.method_handle_for(@object, message)
-
rescue NameError
-
nil
-
end
-
-
# @private
-
1
def add_simple_expectation(method_name, response, location)
-
method_double_for(method_name).configure_method
-
super
-
end
-
-
# @private
-
1
def add_simple_stub(method_name, response)
-
method_double_for(method_name).configure_method
-
super
-
end
-
-
# @private
-
1
def visibility_for(method_name)
-
# We fall back to :public because by default we allow undefined methods
-
# to be stubbed, and when we do so, we make them public.
-
MethodReference.method_visibility_for(@object, method_name) || :public
-
end
-
-
1
def reset
-
@method_doubles.each_value { |d| d.reset }
-
super
-
end
-
-
1
def message_received(message, *args, &block)
-
RSpec::Mocks.space.any_instance_recorders_from_ancestry_of(object).each do |subscriber|
-
subscriber.notify_received_message(object, message, args, block)
-
end
-
super
-
end
-
-
1
private
-
-
1
def any_instance_class_recorder_observing_method?(klass, method_name)
-
only_return_existing = true
-
recorder = ::RSpec::Mocks.space.any_instance_recorder_for(klass, only_return_existing)
-
return true if recorder && recorder.already_observing?(method_name)
-
-
superklass = klass.superclass
-
return false if superklass.nil?
-
any_instance_class_recorder_observing_method?(superklass, method_name)
-
end
-
end
-
-
# @private
-
# When we mock or stub a method on a class, we have to treat it a bit different,
-
# because normally singleton method definitions only affect the object on which
-
# they are defined, but on classes they affect subclasses, too. As a result,
-
# we need some special handling to get the original method.
-
1
module PartialClassDoubleProxyMethods
-
1
def initialize(source_space, *args)
-
@source_space = source_space
-
super(*args)
-
end
-
-
# Consider this situation:
-
#
-
# class A; end
-
# class B < A; end
-
#
-
# allow(A).to receive(:new)
-
# expect(B).to receive(:new).and_call_original
-
#
-
# When getting the original definition for `B.new`, we cannot rely purely on
-
# using `B.method(:new)` before our redefinition is defined on `B`, because
-
# `B.method(:new)` will return a method that will execute the stubbed version
-
# of the method on `A` since singleton methods on classes are in the lookup
-
# hierarchy.
-
#
-
# To do it properly, we need to find the original definition of `new` from `A`
-
# from _before_ `A` was stubbed, and we need to rebind it to `B` so that it will
-
# run with the proper `self`.
-
#
-
# That's what this method (together with `original_unbound_method_handle_from_ancestor_for`)
-
# does.
-
1
def original_method_handle_for(message)
-
unbound_method = superclass_proxy &&
-
superclass_proxy.original_unbound_method_handle_from_ancestor_for(message.to_sym)
-
-
return super unless unbound_method
-
unbound_method.bind(object)
-
# :nocov:
-
skipped
rescue TypeError
-
skipped
if RUBY_VERSION == '1.8.7'
-
skipped
# In MRI 1.8.7, a singleton method on a class cannot be rebound to its subclass
-
skipped
if unbound_method && unbound_method.owner.ancestors.first != unbound_method.owner
-
skipped
# This is a singleton method; we can't do anything with it
-
skipped
# But we can work around this using a different implementation
-
skipped
double = method_double_from_ancestor_for(message)
-
skipped
return object.method(double.method_stasher.stashed_method_name)
-
skipped
end
-
skipped
end
-
skipped
raise
-
# :nocov:
-
end
-
-
1
protected
-
-
1
def original_unbound_method_handle_from_ancestor_for(message)
-
double = method_double_from_ancestor_for(message)
-
double && double.original_method.unbind
-
end
-
-
1
def method_double_from_ancestor_for(message)
-
@method_doubles.fetch(message) do
-
# The fact that there is no method double for this message indicates
-
# that it has not been redefined by rspec-mocks. We need to continue
-
# looking up the ancestor chain.
-
return superclass_proxy &&
-
superclass_proxy.method_double_from_ancestor_for(message)
-
end
-
end
-
-
1
def superclass_proxy
-
return @superclass_proxy if defined?(@superclass_proxy)
-
-
if (superclass = object.superclass)
-
@superclass_proxy = @source_space.superclass_proxy_for(superclass)
-
else
-
@superclass_proxy = nil
-
end
-
end
-
end
-
-
# @private
-
1
class PartialClassDoubleProxy < PartialDoubleProxy
-
1
include PartialClassDoubleProxyMethods
-
end
-
-
# @private
-
1
class ProxyForNil < PartialDoubleProxy
-
1
def initialize(order_group)
-
set_expectation_behavior
-
super(nil, order_group)
-
end
-
-
1
attr_accessor :disallow_expectations
-
1
attr_accessor :warn_about_expectations
-
-
1
def add_message_expectation(method_name, opts={}, &block)
-
warn_or_raise!(method_name)
-
super
-
end
-
-
1
def add_negative_message_expectation(location, method_name, &implementation)
-
warn_or_raise!(method_name)
-
super
-
end
-
-
1
def add_stub(method_name, opts={}, &implementation)
-
warn_or_raise!(method_name)
-
super
-
end
-
-
1
private
-
-
1
def set_expectation_behavior
-
case RSpec::Mocks.configuration.allow_message_expectations_on_nil
-
when false
-
@warn_about_expectations = false
-
@disallow_expectations = true
-
when true
-
@warn_about_expectations = false
-
@disallow_expectations = false
-
else
-
@warn_about_expectations = true
-
@disallow_expectations = false
-
end
-
end
-
-
1
def warn_or_raise!(method_name)
-
# This method intentionally swallows the message when
-
# neither disallow_expectations nor warn_about_expectations
-
# are set to true.
-
if disallow_expectations
-
raise_error(method_name)
-
elsif warn_about_expectations
-
warn(method_name)
-
end
-
end
-
-
1
def warn(method_name)
-
warning_msg = @error_generator.expectation_on_nil_message(method_name)
-
RSpec.warning(warning_msg)
-
end
-
-
1
def raise_error(method_name)
-
@error_generator.raise_expectation_on_nil_error(method_name)
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support 'reentrant_mutex'
-
-
1
module RSpec
-
1
module Mocks
-
# @private
-
# Provides a default space implementation for outside
-
# the scope of an example. Called "root" because it serves
-
# as the root of the space stack.
-
1
class RootSpace
-
1
def proxy_for(*_args)
-
raise_lifecycle_message
-
end
-
-
1
def any_instance_recorder_for(*_args)
-
raise_lifecycle_message
-
end
-
-
1
def any_instance_proxy_for(*_args)
-
raise_lifecycle_message
-
end
-
-
1
def register_constant_mutator(_mutator)
-
raise_lifecycle_message
-
end
-
-
1
def any_instance_recorders_from_ancestry_of(_object)
-
raise_lifecycle_message
-
end
-
-
1
def reset_all
-
end
-
-
1
def verify_all
-
end
-
-
1
def registered?(_object)
-
false
-
end
-
-
1
def superclass_proxy_for(*_args)
-
raise_lifecycle_message
-
end
-
-
1
def new_scope
-
14
Space.new
-
end
-
-
1
private
-
-
1
def raise_lifecycle_message
-
raise OutsideOfExampleError,
-
"The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported."
-
end
-
end
-
-
# @private
-
1
class Space
-
1
attr_reader :proxies, :any_instance_recorders, :proxy_mutex, :any_instance_mutex
-
-
1
def initialize
-
14
@proxies = {}
-
14
@any_instance_recorders = {}
-
14
@constant_mutators = []
-
14
@expectation_ordering = OrderGroup.new
-
14
@proxy_mutex = new_mutex
-
14
@any_instance_mutex = new_mutex
-
end
-
-
1
def new_scope
-
NestedSpace.new(self)
-
end
-
-
1
def verify_all
-
14
proxies.values.each { |proxy| proxy.verify }
-
14
any_instance_recorders.each_value { |recorder| recorder.verify }
-
end
-
-
1
def reset_all
-
14
proxies.each_value { |proxy| proxy.reset }
-
14
@constant_mutators.reverse.each { |mut| mut.idempotently_reset }
-
14
any_instance_recorders.each_value { |recorder| recorder.stop_all_observation! }
-
14
any_instance_recorders.clear
-
end
-
-
1
def register_constant_mutator(mutator)
-
@constant_mutators << mutator
-
end
-
-
1
def constant_mutator_for(name)
-
@constant_mutators.find { |m| m.full_constant_name == name }
-
end
-
-
1
def any_instance_recorder_for(klass, only_return_existing=false)
-
any_instance_mutex.synchronize do
-
id = klass.__id__
-
any_instance_recorders.fetch(id) do
-
return nil if only_return_existing
-
any_instance_recorder_not_found_for(id, klass)
-
end
-
end
-
end
-
-
1
def any_instance_proxy_for(klass)
-
AnyInstance::Proxy.new(any_instance_recorder_for(klass), proxies_of(klass))
-
end
-
-
1
def proxies_of(klass)
-
proxies.values.select { |proxy| klass === proxy.object }
-
end
-
-
1
def proxy_for(object)
-
proxy_mutex.synchronize do
-
id = id_for(object)
-
proxies.fetch(id) { proxy_not_found_for(id, object) }
-
end
-
end
-
-
1
def superclass_proxy_for(klass)
-
proxy_mutex.synchronize do
-
id = id_for(klass)
-
proxies.fetch(id) { superclass_proxy_not_found_for(id, klass) }
-
end
-
end
-
-
1
alias ensure_registered proxy_for
-
-
1
def registered?(object)
-
proxies.key?(id_for object)
-
end
-
-
1
def any_instance_recorders_from_ancestry_of(object)
-
# Optimization: `any_instance` is a feature we generally
-
# recommend not using, so we can often early exit here
-
# without doing an O(N) linear search over the number of
-
# ancestors in the object's class hierarchy.
-
return [] if any_instance_recorders.empty?
-
-
# We access the ancestors through the singleton class, to avoid calling
-
# `class` in case `class` has been stubbed.
-
(class << object; ancestors; end).map do |klass|
-
any_instance_recorders[klass.__id__]
-
end.compact
-
end
-
-
1
private
-
-
1
def new_mutex
-
28
Support::ReentrantMutex.new
-
end
-
-
1
def proxy_not_found_for(id, object)
-
proxies[id] = case object
-
when NilClass then ProxyForNil.new(@expectation_ordering)
-
when TestDouble then object.__build_mock_proxy_unless_expired(@expectation_ordering)
-
when Class
-
class_proxy_with_callback_verification_strategy(object, CallbackInvocationStrategy.new)
-
else
-
if RSpec::Mocks.configuration.verify_partial_doubles?
-
VerifyingPartialDoubleProxy.new(object, @expectation_ordering)
-
else
-
PartialDoubleProxy.new(object, @expectation_ordering)
-
end
-
end
-
end
-
-
1
def superclass_proxy_not_found_for(id, object)
-
raise "superclass_proxy_not_found_for called with something that is not a class" unless Class === object
-
proxies[id] = class_proxy_with_callback_verification_strategy(object, NoCallbackInvocationStrategy.new)
-
end
-
-
1
def class_proxy_with_callback_verification_strategy(object, strategy)
-
if RSpec::Mocks.configuration.verify_partial_doubles?
-
VerifyingPartialClassDoubleProxy.new(
-
self,
-
object,
-
@expectation_ordering,
-
strategy
-
)
-
else
-
PartialClassDoubleProxy.new(self, object, @expectation_ordering)
-
end
-
end
-
-
1
def any_instance_recorder_not_found_for(id, klass)
-
any_instance_recorders[id] = AnyInstance::Recorder.new(klass)
-
end
-
-
1
if defined?(::BasicObject) && !::BasicObject.method_defined?(:__id__) # for 1.9.2
-
require 'securerandom'
-
-
def id_for(object)
-
id = object.__id__
-
-
return id if object.equal?(::ObjectSpace._id2ref(id))
-
# this suggests that object.__id__ is proxying through to some wrapped object
-
-
object.instance_exec do
-
@__id_for_rspec_mocks_space ||= ::SecureRandom.uuid
-
end
-
end
-
else
-
1
def id_for(object)
-
object.__id__
-
end
-
end
-
end
-
-
# @private
-
1
class NestedSpace < Space
-
1
def initialize(parent)
-
@parent = parent
-
super()
-
end
-
-
1
def proxies_of(klass)
-
super + @parent.proxies_of(klass)
-
end
-
-
1
def constant_mutator_for(name)
-
super || @parent.constant_mutator_for(name)
-
end
-
-
1
def registered?(object)
-
super || @parent.registered?(object)
-
end
-
-
1
private
-
-
1
def proxy_not_found_for(id, object)
-
@parent.proxies[id] || super
-
end
-
-
1
def any_instance_recorder_not_found_for(id, klass)
-
@parent.any_instance_recorders[id] || super
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# @api private
-
# Provides methods for enabling and disabling the available syntaxes
-
# provided by rspec-mocks.
-
1
module Syntax
-
# @private
-
1
def self.warn_about_should!
-
1
@warn_about_should = true
-
end
-
-
# @private
-
1
def self.warn_unless_should_configured(method_name , replacement="the new `:expect` syntax or explicitly enable `:should`")
-
if @warn_about_should
-
RSpec.deprecate(
-
"Using `#{method_name}` from rspec-mocks' old `:should` syntax without explicitly enabling the syntax",
-
:replacement => replacement
-
)
-
-
@warn_about_should = false
-
end
-
end
-
-
# @api private
-
# Enables the should syntax (`dbl.stub`, `dbl.should_receive`, etc).
-
1
def self.enable_should(syntax_host=default_should_syntax_host)
-
1
@warn_about_should = false if syntax_host == default_should_syntax_host
-
1
return if should_enabled?(syntax_host)
-
-
1
syntax_host.class_exec do
-
1
def should_receive(message, opts={}, &block)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
::RSpec::Mocks.expect_message(self, message, opts, &block)
-
end
-
-
1
def should_not_receive(message, &block)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
::RSpec::Mocks.expect_message(self, message, {}, &block).never
-
end
-
-
1
def stub(message_or_hash, opts={}, &block)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
if ::Hash === message_or_hash
-
message_or_hash.each { |message, value| stub(message).and_return value }
-
else
-
::RSpec::Mocks.allow_message(self, message_or_hash, opts, &block)
-
end
-
end
-
-
1
def unstub(message)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__, "`allow(...).to receive(...).and_call_original` or explicitly enable `:should`")
-
::RSpec::Mocks.space.proxy_for(self).remove_stub(message)
-
end
-
-
1
def stub_chain(*chain, &blk)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
::RSpec::Mocks::StubChain.stub_chain_on(self, *chain, &blk)
-
end
-
-
1
def as_null_object
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
@_null_object = true
-
::RSpec::Mocks.space.proxy_for(self).as_null_object
-
end
-
-
1
def null_object?
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
defined?(@_null_object)
-
end
-
-
1
def received_message?(message, *args, &block)
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
::RSpec::Mocks.space.proxy_for(self).received_message?(message, *args, &block)
-
end
-
-
1
unless Class.respond_to? :any_instance
-
1
Class.class_exec do
-
1
def any_instance
-
::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__)
-
::RSpec::Mocks.space.any_instance_proxy_for(self)
-
end
-
end
-
end
-
end
-
end
-
-
# @api private
-
# Disables the should syntax (`dbl.stub`, `dbl.should_receive`, etc).
-
1
def self.disable_should(syntax_host=default_should_syntax_host)
-
return unless should_enabled?(syntax_host)
-
-
syntax_host.class_exec do
-
undef should_receive
-
undef should_not_receive
-
undef stub
-
undef unstub
-
undef stub_chain
-
undef as_null_object
-
undef null_object?
-
undef received_message?
-
end
-
-
Class.class_exec do
-
undef any_instance
-
end
-
end
-
-
# @api private
-
# Enables the expect syntax (`expect(dbl).to receive`, `allow(dbl).to receive`, etc).
-
1
def self.enable_expect(syntax_host=::RSpec::Mocks::ExampleMethods)
-
1
return if expect_enabled?(syntax_host)
-
-
1
syntax_host.class_exec do
-
1
def receive(method_name, &block)
-
Matchers::Receive.new(method_name, block)
-
end
-
-
1
def receive_messages(message_return_value_hash)
-
matcher = Matchers::ReceiveMessages.new(message_return_value_hash)
-
matcher.warn_about_block if block_given?
-
matcher
-
end
-
-
1
def receive_message_chain(*messages, &block)
-
Matchers::ReceiveMessageChain.new(messages, &block)
-
end
-
-
1
def allow(target)
-
AllowanceTarget.new(target)
-
end
-
-
1
def expect_any_instance_of(klass)
-
AnyInstanceExpectationTarget.new(klass)
-
end
-
-
1
def allow_any_instance_of(klass)
-
AnyInstanceAllowanceTarget.new(klass)
-
end
-
end
-
-
1
RSpec::Mocks::ExampleMethods::ExpectHost.class_exec do
-
1
def expect(target)
-
ExpectationTarget.new(target)
-
end
-
end
-
end
-
-
# @api private
-
# Disables the expect syntax (`expect(dbl).to receive`, `allow(dbl).to receive`, etc).
-
1
def self.disable_expect(syntax_host=::RSpec::Mocks::ExampleMethods)
-
return unless expect_enabled?(syntax_host)
-
-
syntax_host.class_exec do
-
undef receive
-
undef receive_messages
-
undef receive_message_chain
-
undef allow
-
undef expect_any_instance_of
-
undef allow_any_instance_of
-
end
-
-
RSpec::Mocks::ExampleMethods::ExpectHost.class_exec do
-
undef expect
-
end
-
end
-
-
# @api private
-
# Indicates whether or not the should syntax is enabled.
-
1
def self.should_enabled?(syntax_host=default_should_syntax_host)
-
1
syntax_host.method_defined?(:should_receive)
-
end
-
-
# @api private
-
# Indicates whether or not the expect syntax is enabled.
-
1
def self.expect_enabled?(syntax_host=::RSpec::Mocks::ExampleMethods)
-
1
syntax_host.method_defined?(:allow)
-
end
-
-
# @api private
-
# Determines where the methods like `should_receive`, and `stub` are added.
-
1
def self.default_should_syntax_host
-
# JRuby 1.7.4 introduces a regression whereby `defined?(::BasicObject) => nil`
-
# yet `BasicObject` still exists and patching onto ::Object breaks things
-
# e.g. SimpleDelegator expectations won't work
-
#
-
# See: https://github.com/jruby/jruby/issues/814
-
2
if defined?(JRUBY_VERSION) && JRUBY_VERSION == '1.7.4' && RUBY_VERSION.to_f > 1.8
-
return ::BasicObject
-
end
-
-
# On 1.8.7, Object.ancestors.last == Kernel but
-
# things blow up if we include `RSpec::Mocks::Methods`
-
# into Kernel...not sure why.
-
2
return Object unless defined?(::BasicObject)
-
-
# MacRuby has BasicObject but it's not the root class.
-
2
return Object unless Object.ancestors.last == ::BasicObject
-
-
2
::BasicObject
-
end
-
end
-
end
-
end
-
-
1
if defined?(BasicObject)
-
# The legacy `:should` syntax adds the following methods directly to
-
# `BasicObject` so that they are available off of any object. Note, however,
-
# that this syntax does not always play nice with delegate/proxy objects.
-
# We recommend you use the non-monkeypatching `:expect` syntax instead.
-
# @see Class
-
1
class BasicObject
-
# @method should_receive
-
# Sets an expectation that this object should receive a message before
-
# the end of the example.
-
#
-
# @example
-
# logger = double('logger')
-
# thing_that_logs = ThingThatLogs.new(logger)
-
# logger.should_receive(:log)
-
# thing_that_logs.do_something_that_logs_a_message
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
# @see RSpec::Mocks::ExampleMethods#expect
-
-
# @method should_not_receive
-
# Sets and expectation that this object should _not_ receive a message
-
# during this example.
-
# @see RSpec::Mocks::ExampleMethods#expect
-
-
# @method stub
-
# Tells the object to respond to the message with the specified value.
-
#
-
# @example
-
# counter.stub(:count).and_return(37)
-
# counter.stub(:count => 37)
-
# counter.stub(:count) { 37 }
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
# @see RSpec::Mocks::ExampleMethods#allow
-
-
# @method unstub
-
# Removes a stub. On a double, the object will no longer respond to
-
# `message`. On a real object, the original method (if it exists) is
-
# restored.
-
#
-
# This is rarely used, but can be useful when a stub is set up during a
-
# shared `before` hook for the common case, but you want to replace it
-
# for a special case.
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
-
# @method stub_chain
-
# @overload stub_chain(method1, method2)
-
# @overload stub_chain("method1.method2")
-
# @overload stub_chain(method1, method_to_value_hash)
-
#
-
# Stubs a chain of methods.
-
#
-
# ## Warning:
-
#
-
# Chains can be arbitrarily long, which makes it quite painless to
-
# violate the Law of Demeter in violent ways, so you should consider any
-
# use of `stub_chain` a code smell. Even though not all code smells
-
# indicate real problems (think fluent interfaces), `stub_chain` still
-
# results in brittle examples. For example, if you write
-
# `foo.stub_chain(:bar, :baz => 37)` in a spec and then the
-
# implementation calls `foo.baz.bar`, the stub will not work.
-
#
-
# @example
-
# double.stub_chain("foo.bar") { :baz }
-
# double.stub_chain(:foo, :bar => :baz)
-
# double.stub_chain(:foo, :bar) { :baz }
-
#
-
# # Given any of ^^ these three forms ^^:
-
# double.foo.bar # => :baz
-
#
-
# # Common use in Rails/ActiveRecord:
-
# Article.stub_chain("recent.published") { [Article.new] }
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
# @see RSpec::Mocks::ExampleMethods#receive_message_chain
-
-
# @method as_null_object
-
# Tells the object to respond to all messages. If specific stub values
-
# are declared, they'll work as expected. If not, the receiver is
-
# returned.
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
-
# @method null_object?
-
# Returns true if this object has received `as_null_object`
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
end
-
end
-
-
# The legacy `:should` syntax adds the `any_instance` to `Class`.
-
# We generally recommend you use the newer `:expect` syntax instead,
-
# which allows you to stub any instance of a class using
-
# `allow_any_instance_of(klass)` or mock any instance using
-
# `expect_any_instance_of(klass)`.
-
# @see BasicObject
-
1
class Class
-
# @method any_instance
-
# Used to set stubs and message expectations on any instance of a given
-
# class. Returns a [Recorder](Recorder), which records messages like
-
# `stub` and `should_receive` for later playback on instances of the
-
# class.
-
#
-
# @example
-
# Car.any_instance.should_receive(:go)
-
# race = Race.new
-
# race.cars << Car.new
-
# race.go # assuming this delegates to all of its cars
-
# # this example would pass
-
#
-
# Account.any_instance.stub(:balance) { Money.new(:USD, 25) }
-
# Account.new.balance # => Money.new(:USD, 25))
-
#
-
# @return [Recorder]
-
#
-
# @note This is only available when you have enabled the `should` syntax.
-
# @see RSpec::Mocks::ExampleMethods#expect_any_instance_of
-
# @see RSpec::Mocks::ExampleMethods#allow_any_instance_of
-
end
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
module TargetDelegationClassMethods
-
1
def delegate_to(matcher_method)
-
4
define_method(:to) do |matcher, &block|
-
unless matcher_allowed?(matcher)
-
raise_unsupported_matcher(:to, matcher)
-
end
-
define_matcher(matcher, matcher_method, &block)
-
end
-
end
-
-
1
def delegate_not_to(matcher_method, options={})
-
4
method_name = options.fetch(:from)
-
4
define_method(method_name) do |matcher, &block|
-
case matcher
-
when Matchers::Receive, Matchers::HaveReceived
-
define_matcher(matcher, matcher_method, &block)
-
when Matchers::ReceiveMessages, Matchers::ReceiveMessageChain
-
raise_negation_unsupported(method_name, matcher)
-
else
-
raise_unsupported_matcher(method_name, matcher)
-
end
-
end
-
end
-
-
1
def disallow_negation(method_name)
-
4
define_method(method_name) do |matcher, *_args|
-
raise_negation_unsupported(method_name, matcher)
-
end
-
end
-
end
-
-
# @private
-
1
module TargetDelegationInstanceMethods
-
1
attr_reader :target
-
-
1
private
-
-
1
def matcher_allowed?(matcher)
-
Matchers::Matcher === matcher
-
end
-
-
1
def define_matcher(matcher, name, &block)
-
matcher.__send__(name, target, &block)
-
end
-
-
1
def raise_unsupported_matcher(method_name, matcher)
-
raise UnsupportedMatcherError,
-
"only the `receive`, `have_received` and `receive_messages` matchers are supported " \
-
"with `#{expression}(...).#{method_name}`, but you have provided: #{matcher}"
-
end
-
-
1
def raise_negation_unsupported(method_name, matcher)
-
raise NegationUnsupportedError,
-
"`#{expression}(...).#{method_name} #{matcher.name}` is not supported since it " \
-
"doesn't really make sense. What would it even mean?"
-
end
-
end
-
-
# @private
-
1
class TargetBase
-
1
def initialize(target)
-
@target = target
-
end
-
-
1
extend TargetDelegationClassMethods
-
1
include TargetDelegationInstanceMethods
-
end
-
-
# @private
-
1
module ExpectationTargetMethods
-
1
extend TargetDelegationClassMethods
-
1
include TargetDelegationInstanceMethods
-
-
1
delegate_to :setup_expectation
-
1
delegate_not_to :setup_negative_expectation, :from => :not_to
-
1
delegate_not_to :setup_negative_expectation, :from => :to_not
-
-
1
def expression
-
:expect
-
end
-
end
-
-
# @private
-
1
class ExpectationTarget < TargetBase
-
1
include ExpectationTargetMethods
-
end
-
-
# @private
-
1
class AllowanceTarget < TargetBase
-
1
def expression
-
:allow
-
end
-
-
1
delegate_to :setup_allowance
-
1
disallow_negation :not_to
-
1
disallow_negation :to_not
-
end
-
-
# @private
-
1
class AnyInstanceAllowanceTarget < TargetBase
-
1
def expression
-
:allow_any_instance_of
-
end
-
-
1
delegate_to :setup_any_instance_allowance
-
1
disallow_negation :not_to
-
1
disallow_negation :to_not
-
end
-
-
# @private
-
1
class AnyInstanceExpectationTarget < TargetBase
-
1
def expression
-
:expect_any_instance_of
-
end
-
-
1
delegate_to :setup_any_instance_expectation
-
1
delegate_not_to :setup_any_instance_negative_expectation, :from => :not_to
-
1
delegate_not_to :setup_any_instance_negative_expectation, :from => :to_not
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# Implements the methods needed for a pure test double. RSpec::Mocks::Double
-
# includes this module, and it is provided for cases where you want a
-
# pure test double without subclassing RSpec::Mocks::Double.
-
1
module TestDouble
-
# Creates a new test double with a `name` (that will be used in error
-
# messages only)
-
1
def initialize(name=nil, stubs={})
-
@__expired = false
-
if Hash === name && stubs.empty?
-
stubs = name
-
@name = nil
-
else
-
@name = name
-
end
-
assign_stubs(stubs)
-
end
-
-
# Tells the object to respond to all messages. If specific stub values
-
# are declared, they'll work as expected. If not, the receiver is
-
# returned.
-
1
def as_null_object
-
__mock_proxy.as_null_object
-
end
-
-
# Returns true if this object has received `as_null_object`
-
1
def null_object?
-
__mock_proxy.null_object?
-
end
-
-
# This allows for comparing the mock to other objects that proxy such as
-
# ActiveRecords belongs_to proxy objects. By making the other object run
-
# the comparison, we're sure the call gets delegated to the proxy
-
# target.
-
1
def ==(other)
-
other == __mock_proxy
-
end
-
-
# @private
-
1
def inspect
-
TestDoubleFormatter.format(self)
-
end
-
-
# @private
-
1
def to_s
-
inspect.gsub('<', '[').gsub('>', ']')
-
end
-
-
# @private
-
1
def respond_to?(message, incl_private=false)
-
__mock_proxy.null_object? ? true : super
-
end
-
-
# @private
-
1
def __build_mock_proxy_unless_expired(order_group)
-
__raise_expired_error || __build_mock_proxy(order_group)
-
end
-
-
# @private
-
1
def __disallow_further_usage!
-
@__expired = true
-
end
-
-
# Override for default freeze implementation to prevent freezing of test
-
# doubles.
-
1
def freeze
-
RSpec.warn_with("WARNING: you attempted to freeze a test double. This is explicitly a no-op as freezing doubles can lead to undesired behaviour when resetting tests.")
-
end
-
-
1
private
-
-
1
def method_missing(message, *args, &block)
-
proxy = __mock_proxy
-
proxy.record_message_received(message, *args, &block)
-
-
if proxy.null_object?
-
case message
-
when :to_int then return 0
-
when :to_a, :to_ary then return nil
-
when :to_str then return to_s
-
else return self
-
end
-
end
-
-
# Defined private and protected methods will still trigger `method_missing`
-
# when called publicly. We want ruby's method visibility error to get raised,
-
# so we simply delegate to `super` in that case.
-
# ...well, we would delegate to `super`, but there's a JRuby
-
# bug, so we raise our own visibility error instead:
-
# https://github.com/jruby/jruby/issues/1398
-
visibility = proxy.visibility_for(message)
-
if visibility == :private || visibility == :protected
-
ErrorGenerator.new(self).raise_non_public_error(
-
message, visibility
-
)
-
end
-
-
# Required wrapping doubles in an Array on Ruby 1.9.2
-
raise NoMethodError if [:to_a, :to_ary].include? message
-
proxy.raise_unexpected_message_error(message, args)
-
end
-
-
1
def assign_stubs(stubs)
-
stubs.each_pair do |message, response|
-
__mock_proxy.add_simple_stub(message, response)
-
end
-
end
-
-
1
def __mock_proxy
-
::RSpec::Mocks.space.proxy_for(self)
-
end
-
-
1
def __build_mock_proxy(order_group)
-
TestDoubleProxy.new(self, order_group)
-
end
-
-
1
def __raise_expired_error
-
return false unless @__expired
-
ErrorGenerator.new(self).raise_expired_test_double_error
-
end
-
-
1
def initialize_copy(other)
-
as_null_object if other.null_object?
-
super
-
end
-
end
-
-
# A generic test double object. `double`, `instance_double` and friends
-
# return an instance of this.
-
1
class Double
-
1
include TestDouble
-
end
-
-
# @private
-
1
module TestDoubleFormatter
-
1
def self.format(dbl, unwrap=false)
-
format = "#{type_desc(dbl)}#{verified_module_desc(dbl)} #{name_desc(dbl)}"
-
return format if unwrap
-
"#<#{format}>"
-
end
-
-
1
class << self
-
1
private
-
-
1
def type_desc(dbl)
-
case dbl
-
when InstanceVerifyingDouble then "InstanceDouble"
-
when ClassVerifyingDouble then "ClassDouble"
-
when ObjectVerifyingDouble then "ObjectDouble"
-
else "Double"
-
end
-
end
-
-
# @private
-
1
IVAR_GET = Object.instance_method(:instance_variable_get)
-
-
1
def verified_module_desc(dbl)
-
return nil unless VerifyingDouble === dbl
-
"(#{IVAR_GET.bind(dbl).call(:@doubled_module).description})"
-
end
-
-
1
def name_desc(dbl)
-
return "(anonymous)" unless (name = IVAR_GET.bind(dbl).call(:@name))
-
name.inspect
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_mocks 'verifying_proxy'
-
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
module VerifyingDouble
-
1
def respond_to?(message, include_private=false)
-
return super unless null_object?
-
-
method_ref = __mock_proxy.method_reference[message]
-
-
case method_ref.visibility
-
when :public then true
-
when :private then include_private
-
when :protected then include_private || RUBY_VERSION.to_f < 2.0
-
else !method_ref.unimplemented?
-
end
-
end
-
-
1
def method_missing(message, *args, &block)
-
# Null object conditional is an optimization. If not a null object,
-
# validity of method expectations will have been checked at definition
-
# time.
-
if null_object?
-
if @__sending_message == message
-
__mock_proxy.ensure_implemented(message)
-
else
-
__mock_proxy.ensure_publicly_implemented(message, self)
-
end
-
-
__mock_proxy.validate_arguments!(message, args)
-
end
-
-
super
-
end
-
-
# @private
-
1
module SilentIO
-
1
def self.method_missing(*); end
-
1
def self.respond_to?(*)
-
1
true
-
end
-
end
-
-
# Redefining `__send__` causes ruby to issue a warning.
-
1
old, $stderr = $stderr, SilentIO
-
1
def __send__(name, *args, &block)
-
@__sending_message = name
-
super
-
ensure
-
@__sending_message = nil
-
end
-
1
$stderr = old
-
-
1
def send(name, *args, &block)
-
__send__(name, *args, &block)
-
end
-
-
1
def initialize(doubled_module, *args)
-
@doubled_module = doubled_module
-
-
possible_name = args.first
-
name = if String === possible_name || Symbol === possible_name
-
args.shift
-
end
-
-
super(name, *args)
-
@__sending_message = nil
-
end
-
end
-
-
# A mock providing a custom proxy that can verify the validity of any
-
# method stubs or expectations against the public instance methods of the
-
# given class.
-
#
-
# @private
-
1
class InstanceVerifyingDouble
-
1
include TestDouble
-
1
include VerifyingDouble
-
-
1
def __build_mock_proxy(order_group)
-
VerifyingProxy.new(self, order_group,
-
@doubled_module,
-
InstanceMethodReference
-
)
-
end
-
end
-
-
# An awkward module necessary because we cannot otherwise have
-
# ClassVerifyingDouble inherit from Module and still share these methods.
-
#
-
# @private
-
1
module ObjectVerifyingDoubleMethods
-
1
include TestDouble
-
1
include VerifyingDouble
-
-
1
def as_stubbed_const(options={})
-
ConstantMutator.stub(@doubled_module.const_to_replace, self, options)
-
self
-
end
-
-
1
private
-
-
1
def __build_mock_proxy(order_group)
-
VerifyingProxy.new(self, order_group,
-
@doubled_module,
-
ObjectMethodReference
-
)
-
end
-
end
-
-
# Similar to an InstanceVerifyingDouble, except that it verifies against
-
# public methods of the given object.
-
#
-
# @private
-
1
class ObjectVerifyingDouble
-
1
include ObjectVerifyingDoubleMethods
-
end
-
-
# Effectively the same as an ObjectVerifyingDouble (since a class is a type
-
# of object), except with Module in the inheritance chain so that
-
# transferring nested constants to work.
-
#
-
# @private
-
1
class ClassVerifyingDouble < Module
-
1
include ObjectVerifyingDoubleMethods
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support 'method_signature_verifier'
-
-
1
module RSpec
-
1
module Mocks
-
# A message expectation that knows about the real implementation of the
-
# message being expected, so that it can verify that any expectations
-
# have the valid arguments.
-
# @api private
-
1
class VerifyingMessageExpectation < MessageExpectation
-
# A level of indirection is used here rather than just passing in the
-
# method itself, since method look up is expensive and we only want to
-
# do it if actually needed.
-
#
-
# Conceptually the method reference makes more sense as a constructor
-
# argument since it should be immutable, but it is significantly more
-
# straight forward to build the object in pieces so for now it stays as
-
# an accessor.
-
1
attr_accessor :method_reference
-
-
1
def initialize(*args)
-
super
-
end
-
-
# @private
-
1
def with(*args, &block)
-
super(*args, &block).tap do
-
validate_expected_arguments! do |signature|
-
example_call_site_args = [:an_arg] * signature.min_non_kw_args
-
example_call_site_args << :kw_args_hash if signature.required_kw_args.any?
-
@argument_list_matcher.resolve_expected_args_based_on(example_call_site_args)
-
end
-
end
-
end
-
-
1
private
-
-
1
def validate_expected_arguments!
-
return if method_reference.nil?
-
-
method_reference.with_signature do |signature|
-
args = yield signature
-
verifier = Support::LooseSignatureVerifier.new(signature, args)
-
-
unless verifier.valid?
-
# Fail fast is required, otherwise the message expectation will fail
-
# as well ("expected method not called") and clobber this one.
-
@failed_fast = true
-
@error_generator.raise_invalid_arguments_error(verifier)
-
end
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_mocks 'verifying_message_expectation'
-
1
RSpec::Support.require_rspec_mocks 'method_reference'
-
-
1
module RSpec
-
1
module Mocks
-
# @private
-
1
class CallbackInvocationStrategy
-
1
def call(doubled_module)
-
RSpec::Mocks.configuration.verifying_double_callbacks.each do |block|
-
block.call doubled_module
-
end
-
end
-
end
-
-
# @private
-
1
class NoCallbackInvocationStrategy
-
1
def call(_doubled_module)
-
end
-
end
-
-
# @private
-
1
module VerifyingProxyMethods
-
1
def add_stub(method_name, opts={}, &implementation)
-
ensure_implemented(method_name)
-
super
-
end
-
-
1
def add_simple_stub(method_name, *args)
-
ensure_implemented(method_name)
-
super
-
end
-
-
1
def add_message_expectation(method_name, opts={}, &block)
-
ensure_implemented(method_name)
-
super
-
end
-
-
1
def ensure_implemented(method_name)
-
return unless method_reference[method_name].unimplemented?
-
-
@error_generator.raise_unimplemented_error(
-
@doubled_module,
-
method_name,
-
@object
-
)
-
end
-
-
1
def ensure_publicly_implemented(method_name, _object)
-
ensure_implemented(method_name)
-
visibility = method_reference[method_name].visibility
-
-
return if visibility == :public
-
@error_generator.raise_non_public_error(method_name, visibility)
-
end
-
end
-
-
# A verifying proxy mostly acts like a normal proxy, except that it
-
# contains extra logic to try and determine the validity of any expectation
-
# set on it. This includes whether or not methods have been defined and the
-
# validatiy of arguments on method calls.
-
#
-
# In all other ways this behaves like a normal proxy. It only adds the
-
# verification behaviour to specific methods then delegates to the parent
-
# implementation.
-
#
-
# These checks are only activated if the doubled class has already been
-
# loaded, otherwise they are disabled. This allows for testing in
-
# isolation.
-
#
-
# @private
-
1
class VerifyingProxy < TestDoubleProxy
-
1
include VerifyingProxyMethods
-
-
1
def initialize(object, order_group, doubled_module, method_reference_class)
-
super(object, order_group)
-
@object = object
-
@doubled_module = doubled_module
-
@method_reference_class = method_reference_class
-
-
# A custom method double is required to pass through a way to lookup
-
# methods to determine their parameters. This is only relevant if the doubled
-
# class is loaded.
-
@method_doubles = Hash.new do |h, k|
-
h[k] = VerifyingMethodDouble.new(@object, k, self, method_reference[k])
-
end
-
end
-
-
1
def method_reference
-
@method_reference ||= Hash.new do |h, k|
-
h[k] = @method_reference_class.for(@doubled_module, k)
-
end
-
end
-
-
1
def visibility_for(method_name)
-
method_reference[method_name].visibility
-
end
-
-
1
def validate_arguments!(method_name, args)
-
@method_doubles[method_name].validate_arguments!(args)
-
end
-
end
-
-
# @private
-
1
DEFAULT_CALLBACK_INVOCATION_STRATEGY = CallbackInvocationStrategy.new
-
-
# @private
-
1
class VerifyingPartialDoubleProxy < PartialDoubleProxy
-
1
include VerifyingProxyMethods
-
-
1
def initialize(object, expectation_ordering, optional_callback_invocation_strategy=DEFAULT_CALLBACK_INVOCATION_STRATEGY)
-
super(object, expectation_ordering)
-
@doubled_module = DirectObjectReference.new(object)
-
-
# A custom method double is required to pass through a way to lookup
-
# methods to determine their parameters.
-
@method_doubles = Hash.new do |h, k|
-
h[k] = VerifyingExistingMethodDouble.for(object, k, self)
-
end
-
-
optional_callback_invocation_strategy.call(@doubled_module)
-
end
-
-
1
def method_reference
-
@method_doubles
-
end
-
end
-
-
# @private
-
1
class VerifyingPartialClassDoubleProxy < VerifyingPartialDoubleProxy
-
1
include PartialClassDoubleProxyMethods
-
end
-
-
# @private
-
1
class VerifyingMethodDouble < MethodDouble
-
1
def initialize(object, method_name, proxy, method_reference)
-
super(object, method_name, proxy)
-
@method_reference = method_reference
-
end
-
-
1
def message_expectation_class
-
VerifyingMessageExpectation
-
end
-
-
1
def add_expectation(*args, &block)
-
# explict params necessary for 1.8.7 see #626
-
super(*args, &block).tap { |x| x.method_reference = @method_reference }
-
end
-
-
1
def add_stub(*args, &block)
-
# explict params necessary for 1.8.7 see #626
-
super(*args, &block).tap { |x| x.method_reference = @method_reference }
-
end
-
-
1
def proxy_method_invoked(obj, *args, &block)
-
validate_arguments!(args)
-
super
-
end
-
-
1
def validate_arguments!(actual_args)
-
@method_reference.with_signature do |signature|
-
verifier = Support::StrictSignatureVerifier.new(signature, actual_args)
-
raise ArgumentError, verifier.error_message unless verifier.valid?
-
end
-
end
-
end
-
-
# A VerifyingMethodDouble fetches the method to verify against from the
-
# original object, using a MethodReference. This works for pure doubles,
-
# but when the original object is itself the one being modified we need to
-
# collapse the reference and the method double into a single object so that
-
# we can access the original pristine method definition.
-
#
-
# @private
-
1
class VerifyingExistingMethodDouble < VerifyingMethodDouble
-
1
def initialize(object, method_name, proxy)
-
super(object, method_name, proxy, self)
-
-
@valid_method = object.respond_to?(method_name, true)
-
-
# Trigger an eager find of the original method since if we find it any
-
# later we end up getting a stubbed method with incorrect arity.
-
save_original_implementation_callable!
-
end
-
-
1
def with_signature
-
yield Support::MethodSignature.new(original_implementation_callable)
-
end
-
-
1
def unimplemented?
-
!@valid_method
-
end
-
-
1
def self.for(object, method_name, proxy)
-
if ClassNewMethodReference.applies_to?(method_name) { object }
-
VerifyingExistingClassNewMethodDouble
-
else
-
self
-
end.new(object, method_name, proxy)
-
end
-
end
-
-
# Used in place of a `VerifyingExistingMethodDouble` for the specific case
-
# of mocking or stubbing a `new` method on a class. In this case, we substitute
-
# the method signature from `#initialize` since new's signature is just `*args`.
-
#
-
# @private
-
1
class VerifyingExistingClassNewMethodDouble < VerifyingExistingMethodDouble
-
1
def with_signature
-
yield Support::MethodSignature.new(object.instance_method(:initialize))
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Mocks
-
# Version information for RSpec mocks.
-
1
module Version
-
# Version of RSpec mocks currently in use in SemVer format.
-
1
STRING = '3.5.0'
-
end
-
end
-
end
-
1
module RSpec
-
1
module Support
-
# Provides a means to fuzzy-match between two arbitrary objects.
-
# Understands array/hash nesting. Uses `===` or `==` to
-
# perform the matching.
-
1
module FuzzyMatcher
-
# @api private
-
1
def self.values_match?(expected, actual)
-
2
if Hash === actual
-
return hashes_match?(expected, actual) if Hash === expected
-
2
elsif Array === expected && Enumerable === actual && !(Struct === actual)
-
return arrays_match?(expected, actual.to_a)
-
end
-
-
2
return true if expected == actual
-
-
begin
-
expected === actual
-
rescue ArgumentError
-
# Some objects, like 0-arg lambdas on 1.9+, raise
-
# ArgumentError for `expected === actual`.
-
false
-
end
-
end
-
-
# @private
-
1
def self.arrays_match?(expected_list, actual_list)
-
return false if expected_list.size != actual_list.size
-
-
expected_list.zip(actual_list).all? do |expected, actual|
-
values_match?(expected, actual)
-
end
-
end
-
-
# @private
-
1
def self.hashes_match?(expected_hash, actual_hash)
-
return false if expected_hash.size != actual_hash.size
-
-
expected_hash.all? do |expected_key, expected_value|
-
actual_value = actual_hash.fetch(expected_key) { return false }
-
values_match?(expected_value, actual_value)
-
end
-
end
-
-
1
private_class_method :arrays_match?, :hashes_match?
-
end
-
end
-
end
-
1
module RSpec
-
1
module Support
-
# @private
-
1
def self.matcher_definitions
-
2
@matcher_definitions ||= []
-
end
-
-
# Used internally to break cyclic dependency between mocks, expectations,
-
# and support. We don't currently have a consistent implementation of our
-
# matchers, though we are considering changing that:
-
# https://github.com/rspec/rspec-mocks/issues/513
-
#
-
# @private
-
1
def self.register_matcher_definition(&block)
-
2
matcher_definitions << block
-
end
-
-
# Remove a previously registered matcher. Useful for cleaning up after
-
# yourself in specs.
-
#
-
# @private
-
1
def self.deregister_matcher_definition(&block)
-
matcher_definitions.delete(block)
-
end
-
-
# @private
-
1
def self.is_a_matcher?(object)
-
matcher_definitions.any? { |md| md.call(object) }
-
end
-
-
# @api private
-
#
-
# gives a string representation of an object for use in RSpec descriptions
-
1
def self.rspec_description_for_object(object)
-
if RSpec::Support.is_a_matcher?(object) && object.respond_to?(:description)
-
object.description
-
else
-
object
-
end
-
end
-
end
-
end
-
1
require 'rspec/support'
-
1
RSpec::Support.require_rspec_support "ruby_features"
-
1
RSpec::Support.require_rspec_support "matcher_definition"
-
-
1
module RSpec
-
1
module Support
-
# Extracts info about the number of arguments and allowed/required
-
# keyword args of a given method.
-
#
-
# @private
-
1
class MethodSignature # rubocop:disable ClassLength
-
1
attr_reader :min_non_kw_args, :max_non_kw_args, :optional_kw_args, :required_kw_args
-
-
1
def initialize(method)
-
@method = method
-
@optional_kw_args = []
-
@required_kw_args = []
-
classify_parameters
-
end
-
-
1
def non_kw_args_arity_description
-
case max_non_kw_args
-
when min_non_kw_args then min_non_kw_args.to_s
-
when INFINITY then "#{min_non_kw_args} or more"
-
else "#{min_non_kw_args} to #{max_non_kw_args}"
-
end
-
end
-
-
1
def valid_non_kw_args?(positional_arg_count, optional_max_arg_count=positional_arg_count)
-
return true if positional_arg_count.nil?
-
-
min_non_kw_args <= positional_arg_count &&
-
optional_max_arg_count <= max_non_kw_args
-
end
-
-
1
if RubyFeatures.optional_and_splat_args_supported?
-
1
def description
-
@description ||= begin
-
parts = []
-
-
unless non_kw_args_arity_description == "0"
-
parts << "arity of #{non_kw_args_arity_description}"
-
end
-
-
if @optional_kw_args.any?
-
parts << "optional keyword args (#{@optional_kw_args.map(&:inspect).join(", ")})"
-
end
-
-
if @required_kw_args.any?
-
parts << "required keyword args (#{@required_kw_args.map(&:inspect).join(", ")})"
-
end
-
-
parts << "any additional keyword args" if @allows_any_kw_args
-
-
parts.join(" and ")
-
end
-
end
-
-
1
def missing_kw_args_from(given_kw_args)
-
@required_kw_args - given_kw_args
-
end
-
-
1
def invalid_kw_args_from(given_kw_args)
-
return [] if @allows_any_kw_args
-
given_kw_args - @allowed_kw_args
-
end
-
-
1
def has_kw_args_in?(args)
-
Hash === args.last && could_contain_kw_args?(args)
-
end
-
-
# Without considering what the last arg is, could it
-
# contain keyword arguments?
-
1
def could_contain_kw_args?(args)
-
return false if args.count <= min_non_kw_args
-
@allows_any_kw_args || @allowed_kw_args.any?
-
end
-
-
1
def arbitrary_kw_args?
-
@allows_any_kw_args
-
end
-
-
1
def unlimited_args?
-
@max_non_kw_args == INFINITY
-
end
-
-
1
def classify_parameters
-
optional_non_kw_args = @min_non_kw_args = 0
-
@allows_any_kw_args = false
-
-
@method.parameters.each do |(type, name)|
-
case type
-
# def foo(a:)
-
when :keyreq then @required_kw_args << name
-
# def foo(a: 1)
-
when :key then @optional_kw_args << name
-
# def foo(**kw_args)
-
when :keyrest then @allows_any_kw_args = true
-
# def foo(a)
-
when :req then @min_non_kw_args += 1
-
# def foo(a = 1)
-
when :opt then optional_non_kw_args += 1
-
# def foo(*a)
-
when :rest then optional_non_kw_args = INFINITY
-
end
-
end
-
-
@max_non_kw_args = @min_non_kw_args + optional_non_kw_args
-
@allowed_kw_args = @required_kw_args + @optional_kw_args
-
end
-
else
-
def description
-
"arity of #{non_kw_args_arity_description}"
-
end
-
-
def missing_kw_args_from(_given_kw_args)
-
[]
-
end
-
-
def invalid_kw_args_from(_given_kw_args)
-
[]
-
end
-
-
def has_kw_args_in?(_args)
-
false
-
end
-
-
def could_contain_kw_args?(*)
-
false
-
end
-
-
def arbitrary_kw_args?
-
false
-
end
-
-
def unlimited_args?
-
false
-
end
-
-
def classify_parameters
-
arity = @method.arity
-
if arity < 0
-
# `~` inverts the one's complement and gives us the
-
# number of required args
-
@min_non_kw_args = ~arity
-
@max_non_kw_args = INFINITY
-
else
-
@min_non_kw_args = arity
-
@max_non_kw_args = arity
-
end
-
end
-
end
-
-
1
INFINITY = 1 / 0.0
-
end
-
-
# Some versions of JRuby have a nasty bug we have to work around :(.
-
# https://github.com/jruby/jruby/issues/2816
-
if RSpec::Support::Ruby.jruby? &&
-
1
RubyFeatures.optional_and_splat_args_supported? &&
-
Class.new { attr_writer :foo }.instance_method(:foo=).parameters == []
-
-
class MethodSignature < remove_const(:MethodSignature)
-
private
-
-
def classify_parameters
-
super
-
return unless @method.parameters == [] && @method.arity == 1
-
@max_non_kw_args = @min_non_kw_args = 1
-
end
-
end
-
end
-
-
# Encapsulates expectations about the number of arguments and
-
# allowed/required keyword args of a given method.
-
#
-
# @api private
-
1
class MethodSignatureExpectation
-
1
def initialize
-
@min_count = nil
-
@max_count = nil
-
@keywords = []
-
-
@expect_unlimited_arguments = false
-
@expect_arbitrary_keywords = false
-
end
-
-
1
attr_reader :min_count, :max_count, :keywords
-
-
1
attr_accessor :expect_unlimited_arguments, :expect_arbitrary_keywords
-
-
1
def max_count=(number)
-
raise ArgumentError, 'must be a non-negative integer or nil' \
-
unless number.nil? || (number.is_a?(Integer) && number >= 0)
-
-
@max_count = number
-
end
-
-
1
def min_count=(number)
-
raise ArgumentError, 'must be a non-negative integer or nil' \
-
unless number.nil? || (number.is_a?(Integer) && number >= 0)
-
-
@min_count = number
-
end
-
-
1
def empty?
-
@min_count.nil? &&
-
@keywords.to_a.empty? &&
-
!@expect_arbitrary_keywords &&
-
!@expect_unlimited_arguments
-
end
-
-
1
def keywords=(values)
-
@keywords = values.to_a || []
-
end
-
end
-
-
# Deals with the slightly different semantics of block arguments.
-
# For methods, arguments are required unless a default value is provided.
-
# For blocks, arguments are optional, even if no default value is provided.
-
#
-
# However, we want to treat block args as required since you virtually
-
# always want to pass a value for each received argument and our
-
# `and_yield` has treated block args as required for many years.
-
#
-
# @api private
-
1
class BlockSignature < MethodSignature
-
1
if RubyFeatures.optional_and_splat_args_supported?
-
1
def classify_parameters
-
super
-
@min_non_kw_args = @max_non_kw_args unless @max_non_kw_args == INFINITY
-
end
-
end
-
end
-
-
# Abstract base class for signature verifiers.
-
#
-
# @api private
-
1
class MethodSignatureVerifier
-
1
attr_reader :non_kw_args, :kw_args, :min_non_kw_args, :max_non_kw_args
-
-
1
def initialize(signature, args=[])
-
@signature = signature
-
@non_kw_args, @kw_args = split_args(*args)
-
@min_non_kw_args = @max_non_kw_args = @non_kw_args
-
@arbitrary_kw_args = @unlimited_args = false
-
end
-
-
1
def with_expectation(expectation) # rubocop:disable MethodLength
-
return self unless MethodSignatureExpectation === expectation
-
-
if expectation.empty?
-
@min_non_kw_args = @max_non_kw_args = @non_kw_args = nil
-
@kw_args = []
-
else
-
@min_non_kw_args = @non_kw_args = expectation.min_count || 0
-
@max_non_kw_args = expectation.max_count || @min_non_kw_args
-
-
if RubyFeatures.optional_and_splat_args_supported?
-
@unlimited_args = expectation.expect_unlimited_arguments
-
else
-
@unlimited_args = false
-
end
-
-
if RubyFeatures.kw_args_supported?
-
@kw_args = expectation.keywords
-
@arbitrary_kw_args = expectation.expect_arbitrary_keywords
-
else
-
@kw_args = []
-
@arbitrary_kw_args = false
-
end
-
end
-
-
self
-
end
-
-
1
def valid?
-
missing_kw_args.empty? &&
-
invalid_kw_args.empty? &&
-
valid_non_kw_args? &&
-
arbitrary_kw_args? &&
-
unlimited_args?
-
end
-
-
1
def error_message
-
if missing_kw_args.any?
-
"Missing required keyword arguments: %s" % [
-
missing_kw_args.join(", ")
-
]
-
elsif invalid_kw_args.any?
-
"Invalid keyword arguments provided: %s" % [
-
invalid_kw_args.join(", ")
-
]
-
elsif !valid_non_kw_args?
-
"Wrong number of arguments. Expected %s, got %s." % [
-
@signature.non_kw_args_arity_description,
-
non_kw_args
-
]
-
end
-
end
-
-
1
private
-
-
1
def valid_non_kw_args?
-
@signature.valid_non_kw_args?(min_non_kw_args, max_non_kw_args)
-
end
-
-
1
def missing_kw_args
-
@signature.missing_kw_args_from(kw_args)
-
end
-
-
1
def invalid_kw_args
-
@signature.invalid_kw_args_from(kw_args)
-
end
-
-
1
def arbitrary_kw_args?
-
!@arbitrary_kw_args || @signature.arbitrary_kw_args?
-
end
-
-
1
def unlimited_args?
-
!@unlimited_args || @signature.unlimited_args?
-
end
-
-
1
def split_args(*args)
-
kw_args = if @signature.has_kw_args_in?(args)
-
args.pop.keys
-
else
-
[]
-
end
-
-
[args.length, kw_args]
-
end
-
end
-
-
# Figures out wether a given method can accept various arguments.
-
# Surprisingly non-trivial.
-
#
-
# @private
-
1
StrictSignatureVerifier = MethodSignatureVerifier
-
-
# Allows matchers to be used instead of providing keyword arguments. In
-
# practice, when this happens only the arity of the method is verified.
-
#
-
# @private
-
1
class LooseSignatureVerifier < MethodSignatureVerifier
-
1
private
-
-
1
def split_args(*args)
-
if RSpec::Support.is_a_matcher?(args.last) && @signature.could_contain_kw_args?(args)
-
args.pop
-
@signature = SignatureWithKeywordArgumentsMatcher.new(@signature)
-
end
-
-
super(*args)
-
end
-
-
# If a matcher is used in a signature in place of keyword arguments, all
-
# keyword argument validation needs to be skipped since the matcher is
-
# opaque.
-
#
-
# Instead, keyword arguments will be validated when the method is called
-
# and they are actually known.
-
#
-
# @private
-
1
class SignatureWithKeywordArgumentsMatcher
-
1
def initialize(signature)
-
@signature = signature
-
end
-
-
1
def missing_kw_args_from(_kw_args)
-
[]
-
end
-
-
1
def invalid_kw_args_from(_kw_args)
-
[]
-
end
-
-
1
def non_kw_args_arity_description
-
@signature.non_kw_args_arity_description
-
end
-
-
1
def valid_non_kw_args?(*args)
-
@signature.valid_non_kw_args?(*args)
-
end
-
-
1
def has_kw_args_in?(args)
-
@signature.has_kw_args_in?(args)
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_support 'matcher_definition'
-
-
1
module RSpec
-
1
module Support
-
# Provide additional output details beyond what `inspect` provides when
-
# printing Time, DateTime, or BigDecimal
-
# @api private
-
1
class ObjectFormatter # rubocop:disable Style/ClassLength
-
1
ELLIPSIS = "..."
-
-
1
attr_accessor :max_formatted_output_length
-
-
# Methods are deferred to a default instance of the class to maintain the interface
-
# For example, calling ObjectFormatter.format is still possible
-
1
def self.default_instance
-
@default_instance ||= new
-
end
-
-
1
def self.format(object)
-
default_instance.format(object)
-
end
-
-
1
def self.prepare_for_inspection(object)
-
default_instance.prepare_for_inspection(object)
-
end
-
-
1
def initialize(max_formatted_output_length=200)
-
@max_formatted_output_length = max_formatted_output_length
-
@current_structure_stack = []
-
end
-
-
1
def format(object)
-
if max_formatted_output_length.nil?
-
return prepare_for_inspection(object).inspect
-
else
-
formatted_object = prepare_for_inspection(object).inspect
-
if formatted_object.length < max_formatted_output_length
-
return formatted_object
-
else
-
beginning = formatted_object[0 .. max_formatted_output_length / 2]
-
ending = formatted_object[-max_formatted_output_length / 2 ..-1]
-
return beginning + ELLIPSIS + ending
-
end
-
end
-
end
-
-
# Prepares the provided object to be formatted by wrapping it as needed
-
# in something that, when `inspect` is called on it, will produce the
-
# desired output.
-
#
-
# This allows us to apply the desired formatting to hash/array data structures
-
# at any level of nesting, simply by walking that structure and replacing items
-
# with custom items that have `inspect` defined to return the desired output
-
# for that item. Then we can just use `Array#inspect` or `Hash#inspect` to
-
# format the entire thing.
-
1
def prepare_for_inspection(object)
-
case object
-
when Array
-
prepare_array(object)
-
when Hash
-
prepare_hash(object)
-
else
-
inspector_class = INSPECTOR_CLASSES.find { |inspector| inspector.can_inspect?(object) }
-
inspector_class.new(object, self)
-
end
-
end
-
-
1
def prepare_array(array)
-
with_entering_structure(array) do
-
array.map { |element| prepare_element(element) }
-
end
-
end
-
-
1
def prepare_hash(input_hash)
-
with_entering_structure(input_hash) do
-
input_hash.inject({}) do |output_hash, key_and_value|
-
key, value = key_and_value.map { |element| prepare_element(element) }
-
output_hash[key] = value
-
output_hash
-
end
-
end
-
end
-
-
1
def prepare_element(element)
-
if recursive_structure?(element)
-
case element
-
when Array then InspectableItem.new('[...]')
-
when Hash then InspectableItem.new('{...}')
-
else raise # This won't happen
-
end
-
else
-
prepare_for_inspection(element)
-
end
-
end
-
-
1
def with_entering_structure(structure)
-
@current_structure_stack.push(structure)
-
return_value = yield
-
@current_structure_stack.pop
-
return_value
-
end
-
-
1
def recursive_structure?(object)
-
@current_structure_stack.any? { |seen_structure| seen_structure.equal?(object) }
-
end
-
-
1
InspectableItem = Struct.new(:text) do
-
1
def inspect
-
text
-
end
-
-
1
def pretty_print(pp)
-
pp.text(text)
-
end
-
end
-
-
1
BaseInspector = Struct.new(:object, :formatter) do
-
1
def self.can_inspect?(_object)
-
raise NotImplementedError
-
end
-
-
1
def inspect
-
raise NotImplementedError
-
end
-
-
1
def pretty_print(pp)
-
pp.text(inspect)
-
end
-
end
-
-
1
class TimeInspector < BaseInspector
-
1
FORMAT = "%Y-%m-%d %H:%M:%S"
-
-
1
def self.can_inspect?(object)
-
Time === object
-
end
-
-
1
if Time.method_defined?(:nsec)
-
1
def inspect
-
object.strftime("#{FORMAT}.#{"%09d" % object.nsec} %z")
-
end
-
else # for 1.8.7
-
def inspect
-
object.strftime("#{FORMAT}.#{"%06d" % object.usec} %z")
-
end
-
end
-
end
-
-
1
class DateTimeInspector < BaseInspector
-
1
FORMAT = "%a, %d %b %Y %H:%M:%S.%N %z"
-
-
1
def self.can_inspect?(object)
-
defined?(DateTime) && DateTime === object
-
end
-
-
# ActiveSupport sometimes overrides inspect. If `ActiveSupport` is
-
# defined use a custom format string that includes more time precision.
-
1
def inspect
-
if defined?(ActiveSupport)
-
object.strftime(FORMAT)
-
else
-
object.inspect
-
end
-
end
-
end
-
-
1
class BigDecimalInspector < BaseInspector
-
1
def self.can_inspect?(object)
-
defined?(BigDecimal) && BigDecimal === object
-
end
-
-
1
def inspect
-
"#{object.to_s('F')} (#{object.inspect})"
-
end
-
end
-
-
1
class DescribableMatcherInspector < BaseInspector
-
1
def self.can_inspect?(object)
-
Support.is_a_matcher?(object) && object.respond_to?(:description)
-
end
-
-
1
def inspect
-
object.description
-
end
-
end
-
-
1
class UninspectableObjectInspector < BaseInspector
-
1
OBJECT_ID_FORMAT = '%#016x'
-
-
1
def self.can_inspect?(object)
-
object.inspect
-
false
-
rescue NoMethodError
-
true
-
end
-
-
1
def inspect
-
"#<#{klass}:#{native_object_id}>"
-
end
-
-
1
def klass
-
singleton_class = class << object; self; end
-
singleton_class.ancestors.find { |ancestor| !ancestor.equal?(singleton_class) }
-
end
-
-
# http://stackoverflow.com/a/2818916
-
1
def native_object_id
-
OBJECT_ID_FORMAT % (object.__id__ << 1)
-
rescue NoMethodError
-
# In Ruby 1.9.2, BasicObject responds to none of #__id__, #object_id, #id...
-
'-'
-
end
-
end
-
-
1
class DelegatorInspector < BaseInspector
-
1
def self.can_inspect?(object)
-
defined?(Delegator) && Delegator === object
-
end
-
-
1
def inspect
-
"#<#{object.class}(#{formatter.format(object.__getobj__)})>"
-
end
-
end
-
-
1
class InspectableObjectInspector < BaseInspector
-
1
def self.can_inspect?(object)
-
object.inspect
-
true
-
rescue NoMethodError
-
false
-
end
-
-
1
def inspect
-
object.inspect
-
end
-
end
-
-
1
INSPECTOR_CLASSES = [
-
TimeInspector,
-
DateTimeInspector,
-
BigDecimalInspector,
-
UninspectableObjectInspector,
-
DescribableMatcherInspector,
-
DelegatorInspector,
-
InspectableObjectInspector
-
]
-
end
-
end
-
end
-
# external dependencies
-
1
require 'rack'
-
1
require 'tilt'
-
1
require 'rack/protection'
-
-
# stdlib dependencies
-
1
require 'thread'
-
1
require 'time'
-
1
require 'uri'
-
-
# other files we need
-
1
require 'sinatra/show_exceptions'
-
1
require 'sinatra/ext'
-
1
require 'sinatra/version'
-
-
1
module Sinatra
-
# The request object. See Rack::Request for more info:
-
# http://rubydoc.info/github/rack/rack/master/Rack/Request
-
1
class Request < Rack::Request
-
1
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/
-
1
HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*/
-
-
# Returns an array of acceptable media types for the response
-
1
def accept
-
@env['sinatra.accept'] ||= begin
-
if @env.include? 'HTTP_ACCEPT' and @env['HTTP_ACCEPT'].to_s != ''
-
@env['HTTP_ACCEPT'].to_s.scan(HEADER_VALUE_WITH_PARAMS).
-
map! { |e| AcceptEntry.new(e) }.sort
-
else
-
[AcceptEntry.new('*/*')]
-
end
-
end
-
end
-
-
1
def accept?(type)
-
preferred_type(type).to_s.include?(type)
-
end
-
-
1
def preferred_type(*types)
-
accepts = accept # just evaluate once
-
return accepts.first if types.empty?
-
types.flatten!
-
return types.first if accepts.empty?
-
accepts.detect do |pattern|
-
type = types.detect { |t| File.fnmatch(pattern, t) }
-
return type if type
-
end
-
end
-
-
1
alias secure? ssl?
-
-
1
def forwarded?
-
14
@env.include? "HTTP_X_FORWARDED_HOST"
-
end
-
-
1
def safe?
-
get? or head? or options? or trace?
-
end
-
-
1
def idempotent?
-
safe? or put? or delete? or link? or unlink?
-
end
-
-
1
def link?
-
request_method == "LINK"
-
end
-
-
1
def unlink?
-
request_method == "UNLINK"
-
end
-
-
1
private
-
-
1
class AcceptEntry
-
1
attr_accessor :params
-
1
attr_reader :entry
-
-
1
def initialize(entry)
-
params = entry.scan(HEADER_PARAM).map! do |s|
-
key, value = s.strip.split('=', 2)
-
value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
-
[key, value]
-
end
-
-
@entry = entry
-
@type = entry[/[^;]+/].delete(' ')
-
@params = Hash[params]
-
@q = @params.delete('q') { 1.0 }.to_f
-
end
-
-
1
def <=>(other)
-
other.priority <=> self.priority
-
end
-
-
1
def priority
-
# We sort in descending order; better matches should be higher.
-
[ @q, -@type.count('*'), @params.size ]
-
end
-
-
1
def to_str
-
@type
-
end
-
-
1
def to_s(full = false)
-
full ? entry : to_str
-
end
-
-
1
def respond_to?(*args)
-
super or to_str.respond_to?(*args)
-
end
-
-
1
def method_missing(*args, &block)
-
to_str.send(*args, &block)
-
end
-
end
-
end
-
-
# The response object. See Rack::Response and Rack::Response::Helpers for
-
# more info:
-
# http://rubydoc.info/github/rack/rack/master/Rack/Response
-
# http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers
-
1
class Response < Rack::Response
-
1
DROP_BODY_RESPONSES = [204, 205, 304]
-
1
def initialize(*)
-
71
super
-
71
headers['Content-Type'] ||= 'text/html'
-
end
-
-
1
def body=(value)
-
71
value = value.body while Rack::Response === value
-
71
@body = String === value ? [value.to_str] : value
-
end
-
-
1
def each
-
block_given? ? super : enum_for(:each)
-
end
-
-
1
def finish
-
71
result = body
-
-
71
if drop_content_info?
-
headers.delete "Content-Length"
-
headers.delete "Content-Type"
-
end
-
-
71
if drop_body?
-
close
-
result = []
-
end
-
-
71
if calculate_content_length?
-
# if some other code has already set Content-Length, don't muck with it
-
# currently, this would be the static file-handler
-
128
headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
-
end
-
-
71
[status.to_i, headers, result]
-
end
-
-
1
private
-
-
1
def calculate_content_length?
-
71
headers["Content-Type"] and not headers["Content-Length"] and Array === body
-
end
-
-
1
def drop_content_info?
-
71
status.to_i / 100 == 1 or drop_body?
-
end
-
-
1
def drop_body?
-
142
DROP_BODY_RESPONSES.include?(status.to_i)
-
end
-
end
-
-
# Some Rack handlers (Thin, Rainbows!) implement an extended body object protocol, however,
-
# some middleware (namely Rack::Lint) will break it by not mirroring the methods in question.
-
# This middleware will detect an extended body object and will make sure it reaches the
-
# handler directly. We do this here, so our middleware and middleware set up by the app will
-
# still be able to run.
-
1
class ExtendedRack < Struct.new(:app)
-
1
def call(env)
-
71
result, callback = app.call(env), env['async.callback']
-
71
return result unless callback and async?(*result)
-
after_response { callback.call result }
-
setup_close(env, *result)
-
throw :async
-
end
-
-
1
private
-
-
1
def setup_close(env, status, headers, body)
-
return unless body.respond_to? :close and env.include? 'async.close'
-
env['async.close'].callback { body.close }
-
env['async.close'].errback { body.close }
-
end
-
-
1
def after_response(&block)
-
raise NotImplementedError, "only supports EventMachine at the moment" unless defined? EventMachine
-
EventMachine.next_tick(&block)
-
end
-
-
1
def async?(status, headers, body)
-
return true if status == -1
-
body.respond_to? :callback and body.respond_to? :errback
-
end
-
end
-
-
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
-
# if another CommonLogger is already in the middleware chain.
-
1
class CommonLogger < Rack::CommonLogger
-
1
def call(env)
-
env['sinatra.commonlogger'] ? @app.call(env) : super
-
end
-
-
1
superclass.class_eval do
-
1
alias call_without_check call unless method_defined? :call_without_check
-
1
def call(env)
-
env['sinatra.commonlogger'] = true
-
call_without_check(env)
-
end
-
end
-
end
-
-
1
class NotFound < NameError #:nodoc:
-
29
def http_status; 404 end
-
end
-
-
# Methods available to routes, before/after filters, and views.
-
1
module Helpers
-
# Set or retrieve the response status code.
-
1
def status(value = nil)
-
182
response.status = value if value
-
182
response.status
-
end
-
-
# Set or retrieve the response body. When a block is given,
-
# evaluation is deferred until the body is read with #each.
-
1
def body(value = nil, &block)
-
213
if block_given?
-
def block.each; yield(call) end
-
response.body = block
-
213
elsif value
-
71
headers.delete 'Content-Length' unless request.head? || value.is_a?(Rack::File) || value.is_a?(Stream)
-
71
response.body = value
-
else
-
142
response.body
-
end
-
end
-
-
# Halt processing and redirect to the URI provided.
-
1
def redirect(uri, *args)
-
14
if env['HTTP_VERSION'] == 'HTTP/1.1' and env["REQUEST_METHOD"] != 'GET'
-
14
status 303
-
else
-
status 302
-
end
-
-
# According to RFC 2616 section 14.30, "the field value consists of a
-
# single absolute URI"
-
14
response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?)
-
14
halt(*args)
-
end
-
-
# Generates the absolute URI for a given path in the app.
-
# Takes Rack routers and reverse proxies into account.
-
1
def uri(addr = nil, absolute = true, add_script_name = true)
-
14
return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
-
14
uri = [host = ""]
-
14
if absolute
-
14
host << "http#{'s' if request.secure?}://"
-
14
if request.forwarded? or request.port != (request.secure? ? 443 : 80)
-
14
host << request.host_with_port
-
else
-
host << request.host
-
end
-
end
-
14
uri << request.script_name.to_s if add_script_name
-
14
uri << (addr ? addr : request.path_info).to_s
-
14
File.join uri
-
end
-
-
1
alias url uri
-
1
alias to uri
-
-
# Halt processing and return the error status provided.
-
1
def error(code, body = nil)
-
code, body = 500, code.to_str if code.respond_to? :to_str
-
response.body = body unless body.nil?
-
halt code
-
end
-
-
# Halt processing and return a 404 Not Found.
-
1
def not_found(body = nil)
-
error 404, body
-
end
-
-
# Set multiple response headers with Hash.
-
1
def headers(hash = nil)
-
99
response.headers.merge! hash if hash
-
99
response.headers
-
end
-
-
# Access the underlying Rack session.
-
1
def session
-
49
request.session
-
end
-
-
# Access shared logger object.
-
1
def logger
-
request.logger
-
end
-
-
# Look up a media type by file extension in Rack's mime registry.
-
1
def mime_type(type)
-
71
Base.mime_type(type)
-
end
-
-
# Set the Content-Type of the response body given a media type or file
-
# extension.
-
1
def content_type(type = nil, params = {})
-
71
return response['Content-Type'] unless type
-
71
default = params.delete :default
-
71
mime_type = mime_type(type) || default
-
71
fail "Unknown media type: %p" % type if mime_type.nil?
-
71
mime_type = mime_type.dup
-
355
unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type }
-
71
params[:charset] = params.delete('charset') || settings.default_encoding
-
end
-
71
params.delete :charset if mime_type.include? 'charset'
-
71
unless params.empty?
-
71
mime_type << (mime_type.include?(';') ? ', ' : ';')
-
mime_type << params.map do |key, val|
-
71
val = val.inspect if val =~ /[";,]/
-
71
"#{key}=#{val}"
-
71
end.join(', ')
-
end
-
71
response['Content-Type'] = mime_type
-
end
-
-
# Set the Content-Disposition to "attachment" with the specified filename,
-
# instructing the user agents to prompt to save.
-
1
def attachment(filename = nil, disposition = 'attachment')
-
response['Content-Disposition'] = disposition.to_s
-
if filename
-
params = '; filename="%s"' % File.basename(filename)
-
response['Content-Disposition'] << params
-
ext = File.extname(filename)
-
content_type(ext) unless response['Content-Type'] or ext.empty?
-
end
-
end
-
-
# Use the contents of the file at +path+ as the response body.
-
1
def send_file(path, opts = {})
-
if opts[:type] or not response['Content-Type']
-
content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
-
end
-
-
disposition = opts[:disposition]
-
filename = opts[:filename]
-
disposition = 'attachment' if disposition.nil? and filename
-
filename = path if filename.nil?
-
attachment(filename, disposition) if disposition
-
-
last_modified opts[:last_modified] if opts[:last_modified]
-
-
file = Rack::File.new nil
-
file.path = path
-
result = file.serving env
-
result[1].each { |k,v| headers[k] ||= v }
-
headers['Content-Length'] = result[1]['Content-Length']
-
opts[:status] &&= Integer(opts[:status])
-
halt opts[:status] || result[0], result[2]
-
rescue Errno::ENOENT
-
not_found
-
end
-
-
# Class of the response body in case you use #stream.
-
#
-
# Three things really matter: The front and back block (back being the
-
# block generating content, front the one sending it to the client) and
-
# the scheduler, integrating with whatever concurrency feature the Rack
-
# handler is using.
-
#
-
# Scheduler has to respond to defer and schedule.
-
1
class Stream
-
1
def self.schedule(*) yield end
-
1
def self.defer(*) yield end
-
-
1
def initialize(scheduler = self.class, keep_open = false, &back)
-
@back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
-
@callbacks, @closed = [], false
-
end
-
-
1
def close
-
return if closed?
-
@closed = true
-
@scheduler.schedule { @callbacks.each { |c| c.call }}
-
end
-
-
1
def each(&front)
-
@front = front
-
@scheduler.defer do
-
begin
-
@back.call(self)
-
rescue Exception => e
-
@scheduler.schedule { raise e }
-
end
-
close unless @keep_open
-
end
-
end
-
-
1
def <<(data)
-
@scheduler.schedule { @front.call(data.to_s) }
-
self
-
end
-
-
1
def callback(&block)
-
return yield if closed?
-
@callbacks << block
-
end
-
-
1
alias errback callback
-
-
1
def closed?
-
@closed
-
end
-
end
-
-
# Allows to start sending data to the client even though later parts of
-
# the response body have not yet been generated.
-
#
-
# The close parameter specifies whether Stream#close should be called
-
# after the block has been executed. This is only relevant for evented
-
# servers like Thin or Rainbows.
-
1
def stream(keep_open = false)
-
scheduler = env['async.callback'] ? EventMachine : Stream
-
current = @params.dup
-
body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
-
end
-
-
# Specify response freshness policy for HTTP caches (Cache-Control header).
-
# Any number of non-value directives (:public, :private, :no_cache,
-
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
-
# a Hash of value directives (:max_age, :min_stale, :s_max_age).
-
#
-
# cache_control :public, :must_revalidate, :max_age => 60
-
# => Cache-Control: public, must-revalidate, max-age=60
-
#
-
# See RFC 2616 / 14.9 for more on standard cache control directives:
-
# http://tools.ietf.org/html/rfc2616#section-14.9.1
-
1
def cache_control(*values)
-
if values.last.kind_of?(Hash)
-
hash = values.pop
-
hash.reject! { |k,v| v == false }
-
hash.reject! { |k,v| values << k if v == true }
-
else
-
hash = {}
-
end
-
-
values.map! { |value| value.to_s.tr('_','-') }
-
hash.each do |key, value|
-
key = key.to_s.tr('_', '-')
-
value = value.to_i if key == "max-age"
-
values << "#{key}=#{value}"
-
end
-
-
response['Cache-Control'] = values.join(', ') if values.any?
-
end
-
-
# Set the Expires header and Cache-Control/max-age directive. Amount
-
# can be an integer number of seconds in the future or a Time object
-
# indicating when the response should be considered "stale". The remaining
-
# "values" arguments are passed to the #cache_control helper:
-
#
-
# expires 500, :public, :must_revalidate
-
# => Cache-Control: public, must-revalidate, max-age=60
-
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
-
#
-
1
def expires(amount, *values)
-
values << {} unless values.last.kind_of?(Hash)
-
-
if amount.is_a? Integer
-
time = Time.now + amount.to_i
-
max_age = amount
-
else
-
time = time_for amount
-
max_age = time - Time.now
-
end
-
-
values.last.merge!(:max_age => max_age)
-
cache_control(*values)
-
-
response['Expires'] = time.httpdate
-
end
-
-
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
-
# and halt if conditional GET matches. The +time+ argument is a Time,
-
# DateTime, or other object that responds to +to_time+.
-
#
-
# When the current request includes an 'If-Modified-Since' header that is
-
# equal or later than the time specified, execution is immediately halted
-
# with a '304 Not Modified' response.
-
1
def last_modified(time)
-
return unless time
-
time = time_for time
-
response['Last-Modified'] = time.httpdate
-
return if env['HTTP_IF_NONE_MATCH']
-
-
if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
-
# compare based on seconds since epoch
-
since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
-
halt 304 if since >= time.to_i
-
end
-
-
if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
-
# compare based on seconds since epoch
-
since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
-
halt 412 if since < time.to_i
-
end
-
rescue ArgumentError
-
end
-
-
1
ETAG_KINDS = [:strong, :weak]
-
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
-
# GET matches. The +value+ argument is an identifier that uniquely
-
# identifies the current version of the resource. The +kind+ argument
-
# indicates whether the etag should be used as a :strong (default) or :weak
-
# cache validator.
-
#
-
# When the current request includes an 'If-None-Match' header with a
-
# matching etag, execution is immediately halted. If the request method is
-
# GET or HEAD, a '304 Not Modified' response is sent.
-
1
def etag(value, options = {})
-
# Before touching this code, please double check RFC 2616 14.24 and 14.26.
-
options = {:kind => options} unless Hash === options
-
kind = options[:kind] || :strong
-
new_resource = options.fetch(:new_resource) { request.post? }
-
-
unless ETAG_KINDS.include?(kind)
-
raise ArgumentError, ":strong or :weak expected"
-
end
-
-
value = '"%s"' % value
-
value = "W/#{value}" if kind == :weak
-
response['ETag'] = value
-
-
if success? or status == 304
-
if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
-
halt(request.safe? ? 304 : 412)
-
end
-
-
if env['HTTP_IF_MATCH']
-
halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
-
end
-
end
-
end
-
-
# Sugar for redirect (example: redirect back)
-
1
def back
-
request.referer
-
end
-
-
# whether or not the status is set to 1xx
-
1
def informational?
-
status.between? 100, 199
-
end
-
-
# whether or not the status is set to 2xx
-
1
def success?
-
status.between? 200, 299
-
end
-
-
# whether or not the status is set to 3xx
-
1
def redirect?
-
status.between? 300, 399
-
end
-
-
# whether or not the status is set to 4xx
-
1
def client_error?
-
status.between? 400, 499
-
end
-
-
# whether or not the status is set to 5xx
-
1
def server_error?
-
56
status.between? 500, 599
-
end
-
-
# whether or not the status is set to 404
-
1
def not_found?
-
28
status == 404
-
end
-
-
# Generates a Time object from the given value.
-
# Used by #expires and #last_modified.
-
1
def time_for(value)
-
if value.respond_to? :to_time
-
value.to_time
-
elsif value.is_a? Time
-
value
-
elsif value.respond_to? :new_offset
-
# DateTime#to_time does the same on 1.9
-
d = value.new_offset 0
-
t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
-
t.getlocal
-
elsif value.respond_to? :mday
-
# Date#to_time does the same on 1.9
-
Time.local(value.year, value.mon, value.mday)
-
elsif value.is_a? Numeric
-
Time.at value
-
else
-
Time.parse value.to_s
-
end
-
rescue ArgumentError => boom
-
raise boom
-
rescue Exception
-
raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
-
end
-
-
1
private
-
-
# Helper method checking if a ETag value list includes the current ETag.
-
1
def etag_matches?(list, new_resource = request.post?)
-
return !new_resource if list == '*'
-
list.to_s.split(/\s*,\s*/).include? response['ETag']
-
end
-
-
1
def with_params(temp_params)
-
original, @params = @params, temp_params
-
yield
-
ensure
-
@params = original if original
-
end
-
end
-
-
1
private
-
-
# Template rendering methods. Each method takes the name of a template
-
# to render as a Symbol and returns a String with the rendered output,
-
# as well as an optional hash with additional options.
-
#
-
# `template` is either the name or path of the template as symbol
-
# (Use `:'subdir/myview'` for views in subdirectories), or a string
-
# that will be rendered.
-
#
-
# Possible options are:
-
# :content_type The content type to use, same arguments as content_type.
-
# :layout If set to something falsy, no layout is rendered, otherwise
-
# the specified layout is used (Ignored for `sass` and `less`)
-
# :layout_engine Engine to use for rendering the layout.
-
# :locals A hash with local variables that should be available
-
# in the template
-
# :scope If set, template is evaluate with the binding of the given
-
# object rather than the application instance.
-
# :views Views directory to use.
-
1
module Templates
-
1
module ContentTyped
-
1
attr_accessor :content_type
-
end
-
-
1
def initialize
-
1
super
-
1
@default_layout = :layout
-
1
@preferred_extension = nil
-
end
-
-
1
def erb(template, options = {}, locals = {}, &block)
-
render(:erb, template, options, locals, &block)
-
end
-
-
1
def erubis(template, options = {}, locals = {})
-
warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
-
"If you have Erubis installed, it will be used automatically."
-
render :erubis, template, options, locals
-
end
-
-
1
def haml(template, options = {}, locals = {}, &block)
-
render(:haml, template, options, locals, &block)
-
end
-
-
1
def sass(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :sass, template, options, locals
-
end
-
-
1
def scss(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :scss, template, options, locals
-
end
-
-
1
def less(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :less, template, options, locals
-
end
-
-
1
def stylus(template, options={}, locals={})
-
options.merge! :layout => false, :default_content_type => :css
-
render :styl, template, options, locals
-
end
-
-
1
def builder(template = nil, options = {}, locals = {}, &block)
-
options[:default_content_type] = :xml
-
render_ruby(:builder, template, options, locals, &block)
-
end
-
-
1
def liquid(template, options = {}, locals = {}, &block)
-
render(:liquid, template, options, locals, &block)
-
end
-
-
1
def markdown(template, options = {}, locals = {})
-
render :markdown, template, options, locals
-
end
-
-
1
def textile(template, options = {}, locals = {})
-
render :textile, template, options, locals
-
end
-
-
1
def rdoc(template, options = {}, locals = {})
-
render :rdoc, template, options, locals
-
end
-
-
1
def asciidoc(template, options = {}, locals = {})
-
render :asciidoc, template, options, locals
-
end
-
-
1
def radius(template, options = {}, locals = {})
-
render :radius, template, options, locals
-
end
-
-
1
def markaby(template = nil, options = {}, locals = {}, &block)
-
render_ruby(:mab, template, options, locals, &block)
-
end
-
-
1
def coffee(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :js
-
render :coffee, template, options, locals
-
end
-
-
1
def nokogiri(template = nil, options = {}, locals = {}, &block)
-
options[:default_content_type] = :xml
-
render_ruby(:nokogiri, template, options, locals, &block)
-
end
-
-
1
def slim(template, options = {}, locals = {}, &block)
-
28
render(:slim, template, options, locals, &block)
-
end
-
-
1
def creole(template, options = {}, locals = {})
-
render :creole, template, options, locals
-
end
-
-
1
def mediawiki(template, options = {}, locals = {})
-
render :mediawiki, template, options, locals
-
end
-
-
1
def wlang(template, options = {}, locals = {}, &block)
-
render(:wlang, template, options, locals, &block)
-
end
-
-
1
def yajl(template, options = {}, locals = {})
-
options[:default_content_type] = :json
-
render :yajl, template, options, locals
-
end
-
-
1
def rabl(template, options = {}, locals = {})
-
Rabl.register!
-
render :rabl, template, options, locals
-
end
-
-
# Calls the given block for every possible template file in views,
-
# named name.ext, where ext is registered on engine.
-
1
def find_template(views, name, engine)
-
5
yield ::File.join(views, "#{name}.#{@preferred_extension}")
-
-
if Tilt.respond_to?(:mappings)
-
Tilt.mappings.each do |ext, engines|
-
next unless ext != @preferred_extension and engines.include? engine
-
yield ::File.join(views, "#{name}.#{ext}")
-
end
-
else
-
Tilt.default_mapping.extensions_for(engine).each do |ext|
-
yield ::File.join(views, "#{name}.#{ext}") unless ext == @preferred_extension
-
end
-
end
-
end
-
-
1
private
-
-
# logic shared between builder and nokogiri
-
1
def render_ruby(engine, template, options = {}, locals = {}, &block)
-
options, template = template, nil if template.is_a?(Hash)
-
template = Proc.new { block } if template.nil?
-
render engine, template, options, locals
-
end
-
-
1
def render(engine, data, options = {}, locals = {}, &block)
-
# merge app-level options
-
56
engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
-
56
options.merge!(engine_options) { |key, v1, v2| v1 }
-
-
# extract generic options
-
56
locals = options.delete(:locals) || locals || {}
-
56
views = options.delete(:views) || settings.views || "./views"
-
56
layout = options[:layout]
-
56
layout = false if layout.nil? && options.include?(:layout)
-
56
eat_errors = layout.nil?
-
56
layout = engine_options[:layout] if layout.nil? or (layout == true && engine_options[:layout] != false)
-
56
layout = @default_layout if layout.nil? or layout == true
-
56
layout_options = options.delete(:layout_options) || {}
-
56
content_type = options.delete(:content_type) || options.delete(:default_content_type)
-
56
layout_engine = options.delete(:layout_engine) || engine
-
56
scope = options.delete(:scope) || self
-
56
options.delete(:layout)
-
-
# set some defaults
-
56
options[:outvar] ||= '@_out_buf'
-
56
options[:default_encoding] ||= settings.default_encoding
-
-
# compile and render template
-
56
begin
-
56
layout_was = @default_layout
-
56
@default_layout = false
-
56
template = compile_template(engine, data, options, views)
-
56
output = template.render(scope, locals, &block)
-
ensure
-
56
@default_layout = layout_was
-
end
-
-
# render layout
-
56
if layout
-
28
options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope).
-
merge!(layout_options)
-
84
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
-
end
-
-
28
output.extend(ContentTyped).content_type = content_type if content_type
-
28
output
-
end
-
-
1
def compile_template(engine, data, options, views)
-
56
eat_errors = options.delete :eat_errors
-
56
template_cache.fetch engine, data, options, views do
-
5
template = Tilt[engine]
-
5
raise "Template engine not found: #{engine}" if template.nil?
-
-
5
case data
-
when Symbol
-
5
body, path, line = settings.templates[data]
-
5
if body
-
body = body.call if body.respond_to?(:call)
-
template.new(path, line.to_i, options) { body }
-
else
-
5
found = false
-
5
@preferred_extension = engine.to_s
-
5
find_template(views, data, template) do |file|
-
5
path ||= file # keep the initial path rather than the last one
-
5
if found = File.exist?(file)
-
5
path = file
-
5
break
-
end
-
end
-
5
throw :layout_missing if eat_errors and not found
-
5
template.new(path, 1, options)
-
end
-
when Proc, String
-
body = data.is_a?(String) ? Proc.new { data } : data
-
path, line = settings.caller_locations.first
-
template.new(path, line.to_i, options, &body)
-
else
-
raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
-
end
-
end
-
end
-
end
-
-
# Base class for all Sinatra applications and middleware.
-
1
class Base
-
1
include Rack::Utils
-
1
include Helpers
-
1
include Templates
-
-
1
URI_INSTANCE = URI.const_defined?(:Parser) ? URI::Parser.new : URI
-
-
1
attr_accessor :app, :env, :request, :response, :params
-
1
attr_reader :template_cache
-
-
1
def initialize(app = nil)
-
1
super()
-
1
@app = app
-
1
@template_cache = Tilt::Cache.new
-
1
yield self if block_given?
-
end
-
-
# Rack call interface.
-
1
def call(env)
-
71
dup.call!(env)
-
end
-
-
1
def call!(env) # :nodoc:
-
71
@env = env
-
71
@request = Request.new(env)
-
71
@response = Response.new
-
71
@params = indifferent_params(@request.params)
-
71
template_cache.clear if settings.reload_templates
-
71
force_encoding(@params)
-
-
71
@response['Content-Type'] = nil
-
142
invoke { dispatch! }
-
114
invoke { error_block!(response.status) } unless @env['sinatra.error']
-
-
71
unless @response['Content-Type']
-
71
if Array === body and body[0].respond_to? :content_type
-
content_type body[0].content_type
-
else
-
71
content_type :html
-
end
-
end
-
-
71
@response.finish
-
end
-
-
# Access settings defined with Base.set.
-
1
def self.settings
-
1035
self
-
end
-
-
# Access settings defined with Base.set.
-
1
def settings
-
896
self.class.settings
-
end
-
-
1
def options
-
warn "Sinatra::Base#options is deprecated and will be removed, " \
-
"use #settings instead."
-
settings
-
end
-
-
# Exit the current block, halts any further processing
-
# of the request, and returns the specified response.
-
1
def halt(*response)
-
14
response = response.first if response.length == 1
-
14
throw :halt, response
-
end
-
-
# Pass control to the next matching route.
-
# If there are no more matching routes, Sinatra will
-
# return a 404 response.
-
1
def pass(&block)
-
throw :pass, block
-
end
-
-
# Forward the request to the downstream app -- middleware only.
-
1
def forward
-
fail "downstream app not set" unless @app.respond_to? :call
-
status, headers, body = @app.call env
-
@response.status = status
-
@response.body = body
-
@response.headers.merge! headers
-
nil
-
end
-
-
1
private
-
-
# Run filters defined on the class and all superclasses.
-
1
def filter!(type, base = settings)
-
284
filter! type, base.superclass if base.superclass.respond_to?(:filters)
-
355
base.filters[type].each { |args| process_route(*args) }
-
end
-
-
# Run routes defined on the class and all superclasses.
-
1
def route!(base = settings, pass_block = nil)
-
99
if routes = base.routes[@request.request_method]
-
71
routes.each do |pattern, keys, conditions, block|
-
185
returned_pass_block = process_route(pattern, keys, conditions) do |*args|
-
43
env['sinatra.route'] = block.instance_variable_get(:@route_name)
-
86
route_eval { block[*args] }
-
end
-
-
# don't wipe out pass_block in superclass
-
142
pass_block = returned_pass_block if returned_pass_block
-
end
-
end
-
-
# Run routes defined in superclass.
-
56
if base.superclass.respond_to?(:routes)
-
28
return route!(base.superclass, pass_block)
-
end
-
-
28
route_eval(&pass_block) if pass_block
-
28
route_missing
-
end
-
-
# Run a route block and throw :halt with the result.
-
1
def route_eval
-
43
throw :halt, yield
-
end
-
-
# If the current request matches pattern and conditions, fill params
-
# with keys and call the given block.
-
# Revert params afterwards.
-
#
-
# Returns pass block.
-
1
def process_route(pattern, keys, conditions, block = nil, values = [])
-
256
route = @request.path_info
-
256
route = '/' if route.empty? and not settings.empty_path_info?
-
256
return unless match = pattern.match(route)
-
114
values += match.captures.map! { |v| force_encoding URI_INSTANCE.unescape(v) if v }
-
-
114
if values.any?
-
original, @params = params, params.merge('splat' => [], 'captures' => values)
-
keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v }
-
end
-
-
114
catch(:pass) do
-
114
conditions.each { |c| throw :pass if c.bind(self).call == false }
-
114
block ? block[self, values] : yield(self, values)
-
end
-
ensure
-
256
@params = original if original
-
end
-
-
# No matching route was found or all routes passed. The default
-
# implementation is to forward the request downstream when running
-
# as middleware (@app is non-nil); when no downstream app is set, raise
-
# a NotFound exception. Subclasses can override this method to perform
-
# custom route miss logic.
-
1
def route_missing
-
28
if @app
-
forward
-
else
-
28
raise NotFound
-
end
-
end
-
-
# Attempt to serve static files from public directory. Throws :halt when
-
# a matching file is found, returns nil otherwise.
-
1
def static!(options = {})
-
return if (public_dir = settings.public_folder).nil?
-
path = File.expand_path("#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" )
-
return unless File.file?(path)
-
-
env['sinatra.static_file'] = path
-
cache_control(*settings.static_cache_control) if settings.static_cache_control?
-
send_file path, options.merge(:disposition => nil)
-
end
-
-
# Enable string or symbol key access to the nested params hash.
-
1
def indifferent_params(object)
-
138
case object
-
when Hash
-
71
new_hash = indifferent_hash
-
138
object.each { |key, value| new_hash[key] = indifferent_params(value) }
-
71
new_hash
-
when Array
-
object.map { |item| indifferent_params(item) }
-
else
-
67
object
-
end
-
end
-
-
# Creates a Hash with indifferent access.
-
1
def indifferent_hash
-
132
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
-
end
-
-
# Run the block with 'throw :halt' support and apply result to the response.
-
1
def invoke
-
426
res = catch(:halt) { yield }
-
185
res = [res] if Integer === res or String === res
-
185
if Array === res and Integer === res.first
-
res = res.dup
-
status(res.shift)
-
body(res.pop)
-
headers(*res)
-
185
elsif res.respond_to? :each
-
43
body res
-
end
-
nil # avoid double setting the same response tuple twice
-
end
-
-
# Dispatch a request with error handling.
-
1
def dispatch!
-
71
invoke do
-
71
static! if settings.static? && (request.get? || request.head?)
-
71
filter! :before
-
71
route!
-
end
-
rescue ::Exception => boom
-
56
invoke { handle_exception!(boom) }
-
ensure
-
71
begin
-
71
filter! :after unless env['sinatra.static_file']
-
rescue ::Exception => boom
-
invoke { handle_exception!(boom) } unless @env['sinatra.error']
-
end
-
end
-
-
# Error handling during requests.
-
1
def handle_exception!(boom)
-
28
@env['sinatra.error'] = boom
-
-
28
if boom.respond_to? :http_status
-
28
status(boom.http_status)
-
elsif settings.use_code? and boom.respond_to? :code and boom.code.between? 400, 599
-
status(boom.code)
-
else
-
status(500)
-
end
-
-
28
status(500) unless status.between? 400, 599
-
-
28
if server_error?
-
dump_errors! boom if settings.dump_errors?
-
raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler
-
end
-
-
28
if not_found?
-
28
headers['X-Cascade'] = 'pass' if settings.x_cascade?
-
28
body '<h1>Not Found</h1>'
-
end
-
-
28
res = error_block!(boom.class, boom) || error_block!(status, boom)
-
28
return res if res or not server_error?
-
raise boom if settings.raise_errors? or settings.show_exceptions?
-
error_block! Exception, boom
-
end
-
-
# Find an custom error block for the key(s) specified.
-
1
def error_block!(key, *block_params)
-
155
base = settings
-
155
while base.respond_to?(:errors)
-
310
next base = base.superclass unless args_array = base.errors[key]
-
args_array.reverse_each do |args|
-
first = args == args_array.first
-
args += [block_params]
-
resp = process_route(*args)
-
return resp unless resp.nil? && !first
-
end
-
end
-
155
return false unless key.respond_to? :superclass and key.superclass < Exception
-
56
error_block!(key.superclass, *block_params)
-
end
-
-
1
def dump_errors!(boom)
-
msg = ["#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
-
@env['rack.errors'].puts(msg)
-
end
-
-
1
class << self
-
1
CALLERS_TO_IGNORE = [ # :nodoc:
-
/\/sinatra(\/(base|main|show_exceptions))?\.rb$/, # all sinatra code
-
/lib\/tilt.*\.rb$/, # all tilt code
-
/^\(.*\)$/, # generated code
-
/rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
-
/active_support/, # active_support require hacks
-
/bundler(\/runtime)?\.rb/, # bundler require hacks
-
/<internal:/, # internal in ruby >= 1.9.2
-
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
-
]
-
-
# contrary to what the comment said previously, rubinius never supported this
-
1
if defined?(RUBY_IGNORE_CALLERS)
-
warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
-
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
-
end
-
-
1
attr_reader :routes, :filters, :templates, :errors
-
-
# Removes all routes, filters, middleware and extension hooks from the
-
# current class (not routes/filters/... defined by its superclass).
-
1
def reset!
-
3
@conditions = []
-
3
@routes = {}
-
3
@filters = {:before => [], :after => []}
-
3
@errors = {}
-
3
@middleware = []
-
3
@prototype = nil
-
3
@extensions = []
-
-
3
if superclass.respond_to?(:templates)
-
7
@templates = Hash.new { |hash,key| superclass.templates[key] }
-
else
-
1
@templates = {}
-
end
-
end
-
-
# Extension modules registered on this class and all superclasses.
-
1
def extensions
-
24
if superclass.respond_to?(:extensions)
-
12
(@extensions + superclass.extensions).uniq
-
else
-
12
@extensions
-
end
-
end
-
-
# Middleware used in this class and all superclasses.
-
1
def middleware
-
2
if superclass.respond_to?(:middleware)
-
1
superclass.middleware + @middleware
-
else
-
1
@middleware
-
end
-
end
-
-
# Sets an option to the given value. If the value is a proc,
-
# the proc will be called every time the option is accessed.
-
1
def set(option, value = (not_set = true), ignore_setter = false, &block)
-
58
raise ArgumentError if block and !not_set
-
58
value, not_set = block, false if block
-
-
58
if not_set
-
raise ArgumentError unless option.respond_to?(:each)
-
option.each { |k,v| set(k, v) }
-
return self
-
end
-
-
58
if respond_to?("#{option}=") and not ignore_setter
-
11
return __send__("#{option}=", value)
-
end
-
-
58
setter = proc { |val| set option, val, true }
-
975
getter = proc { value }
-
-
47
case value
-
when Proc
-
12
getter = value
-
when Symbol, Integer, FalseClass, TrueClass, NilClass
-
28
getter = value.inspect
-
when Hash
-
setter = proc do |val|
-
val = value.merge val if Hash === val
-
set option, val, true
-
end
-
end
-
-
47
define_singleton("#{option}=", setter) if setter
-
47
define_singleton(option, getter) if getter
-
47
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
-
47
self
-
end
-
-
# Same as calling `set :option, true` for each of the given options.
-
1
def enable(*opts)
-
4
opts.each { |key| set(key, true) }
-
end
-
-
# Same as calling `set :option, false` for each of the given options.
-
1
def disable(*opts)
-
opts.each { |key| set(key, false) }
-
end
-
-
# Define a custom error handler. Optionally takes either an Exception
-
# class, or an HTTP status code to specify which errors should be
-
# handled.
-
1
def error(*codes, &block)
-
1
args = compile! "ERROR", //, block
-
2
codes = codes.map { |c| Array(c) }.flatten
-
1
codes << Exception if codes.empty?
-
2
codes.each { |c| (@errors[c] ||= []) << args }
-
end
-
-
# Sugar for `error(404) { ... }`
-
1
def not_found(&block)
-
error(404, &block)
-
error(Sinatra::NotFound, &block)
-
end
-
-
# Define a named template. The block must return the template source.
-
1
def template(name, &block)
-
filename, line = caller_locations.first
-
templates[name] = [block, filename, line.to_i]
-
end
-
-
# Define the layout template. The block must return the template source.
-
1
def layout(name = :layout, &block)
-
template name, &block
-
end
-
-
# Load embedded templates from the file; uses the caller's __FILE__
-
# when no file is specified.
-
1
def inline_templates=(file = nil)
-
file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
-
-
begin
-
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
-
app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2)
-
rescue Errno::ENOENT
-
app, data = nil
-
end
-
-
if data
-
if app and app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
-
encoding = $2
-
else
-
encoding = settings.default_encoding
-
end
-
lines = app.count("\n") + 1
-
template = nil
-
force_encoding data, encoding
-
data.each_line do |line|
-
lines += 1
-
if line =~ /^@@\s*(.*\S)\s*$/
-
template = force_encoding('', encoding)
-
templates[$1.to_sym] = [template, file, lines]
-
elsif template
-
template << line
-
end
-
end
-
end
-
end
-
-
# Lookup or register a mime type in Rack's mime registry.
-
1
def mime_type(type, value = nil)
-
71
return type if type.nil?
-
71
return type.to_s if type.to_s.include?('/')
-
71
type = ".#{type}" unless type.to_s[0] == ?.
-
71
return Rack::Mime.mime_type(type, nil) unless value
-
Rack::Mime::MIME_TYPES[type] = value
-
end
-
-
# provides all mime types matching type, including deprecated types:
-
# mime_types :html # => ['text/html']
-
# mime_types :js # => ['application/javascript', 'text/javascript']
-
1
def mime_types(type)
-
type = mime_type type
-
type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
-
end
-
-
# Define a before filter; runs before all requests within the same
-
# context as route handlers and may access/modify the request and
-
# response.
-
1
def before(path = nil, options = {}, &block)
-
add_filter(:before, path, options, &block)
-
end
-
-
# Define an after filter; runs after all requests within the same
-
# context as route handlers and may access/modify the request and
-
# response.
-
1
def after(path = nil, options = {}, &block)
-
2
add_filter(:after, path, options, &block)
-
end
-
-
# add a filter
-
1
def add_filter(type, path = nil, options = {}, &block)
-
2
path, options = //, path if path.respond_to?(:each_pair)
-
2
filters[type] << compile!(type, path || //, block, options)
-
end
-
-
# Add a route condition. The route is considered non-matching when the
-
# block returns false.
-
1
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
-
@conditions << generate_method(name, &block)
-
end
-
-
1
def public=(value)
-
warn ":public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead"
-
set(:public_folder, value)
-
end
-
-
1
def public_dir=(value)
-
self.public_folder = value
-
end
-
-
1
def public_dir
-
public_folder
-
end
-
-
# Defining a `GET` handler also automatically defines
-
# a `HEAD` handler.
-
1
def get(path, opts = {}, &block)
-
4
conditions = @conditions.dup
-
4
route('GET', path, opts, &block)
-
-
4
@conditions = conditions
-
4
route('HEAD', path, opts, &block)
-
end
-
-
1
def put(path, opts = {}, &bk) route 'PUT', path, opts, &bk end
-
4
def post(path, opts = {}, &bk) route 'POST', path, opts, &bk end
-
2
def delete(path, opts = {}, &bk) route 'DELETE', path, opts, &bk end
-
1
def head(path, opts = {}, &bk) route 'HEAD', path, opts, &bk end
-
1
def options(path, opts = {}, &bk) route 'OPTIONS', path, opts, &bk end
-
1
def patch(path, opts = {}, &bk) route 'PATCH', path, opts, &bk end
-
1
def link(path, opts = {}, &bk) route 'LINK', path, opts, &bk end
-
1
def unlink(path, opts = {}, &bk) route 'UNLINK', path, opts, &bk end
-
-
# Makes the methods defined in the block and in the Modules given
-
# in `extensions` available to the handlers and templates
-
1
def helpers(*extensions, &block)
-
6
class_eval(&block) if block_given?
-
6
include(*extensions) if extensions.any?
-
end
-
-
# Register an extension. Alternatively take a block from which an
-
# extension will be created and registered on the fly.
-
1
def register(*extensions, &block)
-
4
extensions << Module.new(&block) if block_given?
-
4
@extensions += extensions
-
4
extensions.each do |extension|
-
4
extend extension
-
4
extension.registered(self) if extension.respond_to?(:registered)
-
end
-
end
-
-
73
def development?; environment == :development end
-
1
def production?; environment == :production end
-
1
def test?; environment == :test end
-
-
# Set configuration options for Sinatra and/or the app.
-
# Allows scoping of settings for certain environments.
-
1
def configure(*envs)
-
1
yield self if envs.empty? || envs.include?(environment.to_sym)
-
end
-
-
# Use the specified Rack middleware
-
1
def use(middleware, *args, &block)
-
1
@prototype = nil
-
1
@middleware << [middleware, args, block]
-
end
-
-
# Stop the self-hosted server if running.
-
1
def quit!
-
return unless running?
-
# Use Thin's hard #stop! if available, otherwise just #stop.
-
running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop
-
$stderr.puts "== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
-
set :running_server, nil
-
set :handler_name, nil
-
end
-
-
1
alias_method :stop!, :quit!
-
-
# Run the Sinatra app as a self-hosted server using
-
# Thin, Puma, Mongrel, or WEBrick (in that order). If given a block, will call
-
# with the constructed handler once we have taken the stage.
-
1
def run!(options = {}, &block)
-
return if running?
-
set options
-
handler = detect_rack_handler
-
handler_name = handler.name.gsub(/.*::/, '')
-
server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
-
server_settings.merge!(:Port => port, :Host => bind)
-
-
begin
-
start_server(handler, server_settings, handler_name, &block)
-
rescue Errno::EADDRINUSE
-
$stderr.puts "== Someone is already performing on port #{port}!"
-
raise
-
ensure
-
quit!
-
end
-
end
-
-
1
alias_method :start!, :run!
-
-
# Check whether the self-hosted server is running or not.
-
1
def running?
-
running_server?
-
end
-
-
# The prototype instance used to process requests.
-
1
def prototype
-
71
@prototype ||= new
-
end
-
-
# Create a new instance without middleware in front of it.
-
1
alias new! new unless method_defined? :new!
-
-
# Create a new instance of the class fronted by its middleware
-
# pipeline. The object is guaranteed to respond to #call but may not be
-
# an instance of the class new was called on.
-
1
def new(*args, &bk)
-
1
instance = new!(*args, &bk)
-
1
Wrapper.new(build(instance).to_app, instance)
-
end
-
-
# Creates a Rack::Builder instance with all the middleware set up and
-
# the given +app+ as end point.
-
1
def build(app)
-
1
builder = Rack::Builder.new
-
1
setup_default_middleware builder
-
1
setup_middleware builder
-
1
builder.run app
-
1
builder
-
end
-
-
1
def call(env)
-
142
synchronize { prototype.call(env) }
-
end
-
-
# Like Kernel#caller but excluding certain magic entries and without
-
# line / method information; the resulting array contains filenames only.
-
1
def caller_files
-
2
cleaned_caller(1).flatten
-
end
-
-
# Like caller_files, but containing Arrays rather than strings with the
-
# first element being the file, and the second being the line.
-
1
def caller_locations
-
cleaned_caller 2
-
end
-
-
1
private
-
-
# Starts the server by running the Rack Handler.
-
1
def start_server(handler, server_settings, handler_name)
-
handler.run(self, server_settings) do |server|
-
unless handler_name =~ /cgi/i
-
$stderr.puts "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
-
end
-
-
setup_traps
-
set :running_server, server
-
set :handler_name, handler_name
-
server.threaded = settings.threaded if server.respond_to? :threaded=
-
-
yield server if block_given?
-
end
-
end
-
-
1
def setup_traps
-
if traps?
-
at_exit { quit! }
-
-
[:INT, :TERM].each do |signal|
-
old_handler = trap(signal) do
-
quit!
-
old_handler.call if old_handler.respond_to?(:call)
-
end
-
end
-
-
set :traps, false
-
end
-
end
-
-
# Dynamically defines a method on settings.
-
1
def define_singleton(name, content = Proc.new)
-
# replace with call to singleton_class once we're 1.9 only
-
282
(class << self; self; end).class_eval do
-
141
undef_method(name) if method_defined? name
-
141
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
-
end
-
end
-
-
# Condition for matching host name. Parameter might be String or Regexp.
-
1
def host_name(pattern)
-
condition { pattern === request.host }
-
end
-
-
# Condition for matching user agent. Parameter should be Regexp.
-
# Will set params[:agent].
-
1
def user_agent(pattern)
-
condition do
-
if request.user_agent.to_s =~ pattern
-
@params[:agent] = $~[1..-1]
-
true
-
else
-
false
-
end
-
end
-
end
-
1
alias_method :agent, :user_agent
-
-
# Condition for matching mimetypes. Accepts file extensions.
-
1
def provides(*types)
-
types.map! { |t| mime_types(t) }
-
types.flatten!
-
condition do
-
if type = response['Content-Type']
-
types.include? type or types.include? type[/^[^;]+/]
-
elsif type = request.preferred_type(types)
-
params = (type.respond_to?(:params) ? type.params : {})
-
content_type(type, params)
-
true
-
else
-
false
-
end
-
end
-
end
-
-
1
def route(verb, path, options = {}, &block)
-
# Because of self.options.host
-
12
host_name(options.delete(:host)) if options.key?(:host)
-
12
enable :empty_path_info if path == "" and empty_path_info.nil?
-
12
signature = compile!(verb, path, block, options)
-
12
(@routes[verb] ||= []) << signature
-
12
invoke_hook(:route_added, verb, path, block)
-
12
signature
-
end
-
-
1
def invoke_hook(name, *args)
-
36
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
-
end
-
-
1
def generate_method(method_name, &block)
-
15
method_name = method_name.to_sym
-
15
define_method(method_name, &block)
-
15
method = instance_method method_name
-
15
remove_method method_name
-
15
method
-
end
-
-
1
def compile!(verb, path, block, options = {})
-
15
options.each_pair { |option, args| send(option, *args) }
-
15
method_name = "#{verb} #{path}"
-
15
unbound_method = generate_method(method_name, &block)
-
15
pattern, keys = compile path
-
15
conditions, @conditions = @conditions, []
-
-
15
wrapper = block.arity != 0 ?
-
proc { |a,p| unbound_method.bind(a).call(*p) } :
-
114
proc { |a,p| unbound_method.bind(a).call }
-
15
wrapper.instance_variable_set(:@route_name, method_name)
-
-
15
[ pattern, keys, conditions, wrapper ]
-
end
-
-
1
def compile(path)
-
15
if path.respond_to? :to_str
-
12
keys = []
-
-
# Split the path into pieces in between forward slashes.
-
# A negative number is given as the second argument of path.split
-
# because with this number, the method does not ignore / at the end
-
# and appends an empty string at the end of the return value.
-
#
-
12
segments = path.split('/', -1).map! do |segment|
-
30
ignore = []
-
-
# Special character handling.
-
#
-
30
pattern = segment.to_str.gsub(/[^\?\%\\\/\:\*\w]|:(?!\w)/) do |c|
-
ignore << escaped(c).join if c.match(/[\.@]/)
-
patt = encoded(c)
-
patt.gsub(/%[\da-fA-F]{2}/) do |match|
-
match.split(//).map! { |char| char == char.downcase ? char : "[#{char}#{char.downcase}]" }.join
-
end
-
end
-
-
30
ignore = ignore.uniq.join
-
-
# Key handling.
-
#
-
30
pattern.gsub(/((:\w+)|\*)/) do |match|
-
if match == "*"
-
keys << 'splat'
-
"(.*?)"
-
else
-
keys << $2[1..-1]
-
ignore_pattern = safe_ignore(ignore)
-
-
ignore_pattern
-
end
-
end
-
end
-
-
# Special case handling.
-
#
-
12
if last_segment = segments[-1] and last_segment.match(/\[\^\\\./)
-
parts = last_segment.rpartition(/\[\^\\\./)
-
parts[1] = '[^'
-
segments[-1] = parts.join
-
end
-
12
[/\A#{segments.join('/')}\z/, keys]
-
3
elsif path.respond_to?(:keys) && path.respond_to?(:match)
-
[path, path.keys]
-
3
elsif path.respond_to?(:names) && path.respond_to?(:match)
-
3
[path, path.names]
-
elsif path.respond_to? :match
-
[path, []]
-
else
-
raise TypeError, path
-
end
-
end
-
-
1
def encoded(char)
-
enc = URI_INSTANCE.escape(char)
-
enc = "(?:#{escaped(char, enc).join('|')})" if enc == char
-
enc = "(?:#{enc}|#{encoded('+')})" if char == " "
-
enc
-
end
-
-
1
def escaped(char, enc = URI_INSTANCE.escape(char))
-
[Regexp.escape(enc), URI_INSTANCE.escape(char, /./)]
-
end
-
-
1
def safe_ignore(ignore)
-
unsafe_ignore = []
-
ignore = ignore.gsub(/%[\da-fA-F]{2}/) do |hex|
-
unsafe_ignore << hex[1..2]
-
''
-
end
-
unsafe_patterns = unsafe_ignore.map! do |unsafe|
-
chars = unsafe.split(//).map! do |char|
-
char == char.downcase ? char : char + char.downcase
-
end
-
-
"|(?:%[^#{chars[0]}].|%[#{chars[0]}][^#{chars[1]}])"
-
end
-
if unsafe_patterns.length > 0
-
"((?:[^#{ignore}/?#%]#{unsafe_patterns.join()})+)"
-
else
-
"([^#{ignore}/?#]+)"
-
end
-
end
-
-
1
def setup_default_middleware(builder)
-
1
builder.use ExtendedRack
-
1
builder.use ShowExceptions if show_exceptions?
-
1
builder.use Rack::MethodOverride if method_override?
-
1
builder.use Rack::Head
-
1
setup_logging builder
-
1
setup_sessions builder
-
1
setup_protection builder
-
end
-
-
1
def setup_middleware(builder)
-
2
middleware.each { |c,a,b| builder.use(c, *a, &b) }
-
end
-
-
1
def setup_logging(builder)
-
1
if logging?
-
setup_common_logger(builder)
-
setup_custom_logger(builder)
-
1
elsif logging == false
-
1
setup_null_logger(builder)
-
end
-
end
-
-
1
def setup_null_logger(builder)
-
1
builder.use Rack::NullLogger
-
end
-
-
1
def setup_common_logger(builder)
-
builder.use Sinatra::CommonLogger
-
end
-
-
1
def setup_custom_logger(builder)
-
if logging.respond_to? :to_int
-
builder.use Rack::Logger, logging
-
else
-
builder.use Rack::Logger
-
end
-
end
-
-
1
def setup_protection(builder)
-
1
return unless protection?
-
1
options = Hash === protection ? protection.dup : {}
-
2
protect_session = options.fetch(:session) { sessions? }
-
1
options[:except] = Array options[:except]
-
1
options[:except] += [:session_hijacking, :remote_token] unless protect_session
-
1
options[:reaction] ||= :drop_session
-
1
builder.use Rack::Protection, options
-
end
-
-
1
def setup_sessions(builder)
-
1
return unless sessions?
-
1
options = {}
-
1
options[:secret] = session_secret if session_secret?
-
1
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
-
1
builder.use Rack::Session::Cookie, options
-
end
-
-
1
def detect_rack_handler
-
servers = Array(server)
-
servers.each do |server_name|
-
begin
-
return Rack::Handler.get(server_name.to_s)
-
rescue LoadError, NameError
-
rescue ArgumentError
-
Sinatra::Ext.get_handler(server_name.to_s)
-
end
-
end
-
fail "Server handler (#{servers.join(',')}) not found."
-
end
-
-
1
def inherited(subclass)
-
2
subclass.reset!
-
2
subclass.set :app_file, caller_files.first unless subclass.app_file?
-
2
super
-
end
-
-
1
@@mutex = Mutex.new
-
1
def synchronize(&block)
-
71
if lock?
-
@@mutex.synchronize(&block)
-
else
-
71
yield
-
end
-
end
-
-
# used for deprecation warnings
-
1
def warn(message)
-
super message + "\n\tfrom #{cleaned_caller.first.join(':')}"
-
end
-
-
# Like Kernel#caller but excluding certain magic entries
-
1
def cleaned_caller(keep = 3)
-
caller(1).
-
49
map! { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
-
401
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
-
end
-
end
-
-
# Fixes encoding issues by
-
# * defaulting to UTF-8
-
# * casting params to Encoding.default_external
-
#
-
# The latter might not be necessary if Rack handles it one day.
-
# Keep an eye on Rack's LH #100.
-
72
def force_encoding(*args) settings.force_encoding(*args) end
-
1
if defined? Encoding
-
1
def self.force_encoding(data, encoding = default_encoding)
-
138
return if data == settings || data.is_a?(Tempfile)
-
138
if data.respond_to? :force_encoding
-
67
data.force_encoding(encoding).encode!
-
71
elsif data.respond_to? :each_value
-
138
data.each_value { |v| force_encoding(v, encoding) }
-
elsif data.respond_to? :each
-
data.each { |v| force_encoding(v, encoding) }
-
end
-
138
data
-
end
-
else
-
def self.force_encoding(data, *) data end
-
end
-
-
1
reset!
-
-
1
set :environment, (ENV['RACK_ENV'] || :development).to_sym
-
1
set :raise_errors, Proc.new { test? }
-
1
set :dump_errors, Proc.new { !test? }
-
2
set :show_exceptions, Proc.new { development? }
-
1
set :sessions, false
-
1
set :logging, false
-
1
set :protection, true
-
1
set :method_override, false
-
1
set :use_code, false
-
1
set :default_encoding, "utf-8"
-
1
set :x_cascade, true
-
4
set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
-
1
settings.add_charset << /^text\//
-
-
# explicitly generating a session secret eagerly to play nice with preforking
-
1
begin
-
1
require 'securerandom'
-
1
set :session_secret, SecureRandom.hex(64)
-
rescue LoadError, NotImplementedError
-
# SecureRandom raises a NotImplementedError if no random device is available
-
set :session_secret, "%064x" % Kernel.rand(2**256-1)
-
end
-
-
1
class << self
-
1
alias_method :methodoverride?, :method_override?
-
1
alias_method :methodoverride=, :method_override=
-
end
-
-
1
set :run, false # start server via at-exit hook?
-
1
set :running_server, nil
-
1
set :handler_name, nil
-
1
set :traps, true
-
1
set :server, %w[HTTP webrick]
-
1
set :bind, Proc.new { development? ? 'localhost' : '0.0.0.0' }
-
1
set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
-
-
1
ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
-
1
if ruby_engine == 'macruby'
-
server.unshift 'control_tower'
-
else
-
1
server.unshift 'reel'
-
1
server.unshift 'mongrel' if ruby_engine.nil?
-
1
server.unshift 'puma' if ruby_engine != 'rbx'
-
1
server.unshift 'thin' if ruby_engine != 'jruby'
-
1
server.unshift 'puma' if ruby_engine == 'rbx'
-
1
server.unshift 'trinidad' if ruby_engine == 'jruby'
-
end
-
-
1
set :absolute_redirects, true
-
1
set :prefixed_redirects, false
-
1
set :empty_path_info, nil
-
-
1
set :app_file, nil
-
341
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
-
29
set :views, Proc.new { root && File.join(root, 'views') }
-
72
set :reload_templates, Proc.new { development? }
-
1
set :lock, false
-
1
set :threaded, true
-
-
143
set :public_folder, Proc.new { root && File.join(root, 'public') }
-
72
set :static, Proc.new { public_folder && File.exist?(public_folder) }
-
1
set :static_cache_control, false
-
-
1
error ::Exception do
-
response.status = 500
-
content_type 'text/html'
-
'<h1>Internal Server Error</h1>'
-
end
-
-
1
configure :development do
-
get '/__sinatra__/:image.png' do
-
filename = File.dirname(__FILE__) + "/images/#{params[:image].to_i}.png"
-
content_type :png
-
send_file filename
-
end
-
-
error NotFound do
-
content_type 'text/html'
-
-
if self.class == Sinatra::Application
-
code = <<-RUBY.gsub(/^ {12}/, '')
-
#{request.request_method.downcase} '#{request.path_info}' do
-
"Hello World"
-
end
-
RUBY
-
else
-
code = <<-RUBY.gsub(/^ {12}/, '')
-
class #{self.class}
-
#{request.request_method.downcase} '#{request.path_info}' do
-
"Hello World"
-
end
-
end
-
RUBY
-
-
file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(/^\//, '')
-
code = "# in #{file}\n#{code}" unless file.empty?
-
end
-
-
(<<-HTML).gsub(/^ {10}/, '')
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<style type="text/css">
-
body { text-align:center;font-family:helvetica,arial;font-size:22px;
-
color:#888;margin:20px}
-
#c {margin:0 auto;width:500px;text-align:left}
-
</style>
-
</head>
-
<body>
-
<h2>Sinatra doesn’t know this ditty.</h2>
-
<img src='#{uri "/__sinatra__/404.png"}'>
-
<div id="c">
-
Try this:
-
<pre>#{Rack::Utils.escape_html(code)}</pre>
-
</div>
-
</body>
-
</html>
-
HTML
-
end
-
end
-
end
-
-
# Execution context for classic style (top-level) applications. All
-
# DSL methods executed on main are delegated to this class.
-
#
-
# The Application class should not be subclassed, unless you want to
-
# inherit all settings, routes, handlers, and error pages from the
-
# top-level. Subclassing Sinatra::Base is highly recommended for
-
# modular applications.
-
1
class Application < Base
-
1
set :logging, Proc.new { ! test? }
-
1
set :method_override, true
-
1
set :run, Proc.new { ! test? }
-
1
set :session_secret, Proc.new { super() unless development? }
-
1
set :app_file, nil
-
-
1
def self.register(*extensions, &block) #:nodoc:
-
4
added_methods = extensions.map {|m| m.public_instance_methods }.flatten
-
2
Delegator.delegate(*added_methods)
-
2
super(*extensions, &block)
-
end
-
end
-
-
# Sinatra delegation mixin. Mixing this module into an object causes all
-
# methods to be delegated to the Sinatra::Application class. Used primarily
-
# at the top-level.
-
1
module Delegator #:nodoc:
-
1
def self.delegate(*methods)
-
3
methods.each do |method_name|
-
27
define_method(method_name) do |*args, &block|
-
return super(*args, &block) if respond_to? method_name
-
Delegator.target.send(method_name, *args, &block)
-
end
-
27
private method_name
-
end
-
end
-
-
1
delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
-
:template, :layout, :before, :after, :error, :not_found, :configure,
-
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
-
:production?, :helpers, :settings, :register
-
-
1
class << self
-
1
attr_accessor :target
-
end
-
-
1
self.target = Application
-
end
-
-
1
class Wrapper
-
1
def initialize(stack, instance)
-
1
@stack, @instance = stack, instance
-
end
-
-
1
def settings
-
@instance.settings
-
end
-
-
1
def helpers
-
@instance
-
end
-
-
1
def call(env)
-
71
@stack.call(env)
-
end
-
-
1
def inspect
-
"#<#{@instance.class} app_file=#{settings.app_file.inspect}>"
-
end
-
end
-
-
# Create a new Sinatra application; the block is evaluated in the class scope.
-
1
def self.new(base = Base, &block)
-
base = Class.new(base)
-
base.class_eval(&block) if block_given?
-
base
-
end
-
-
# Extend the top-level DSL with the modules provided.
-
1
def self.register(*extensions, &block)
-
2
Delegator.target.register(*extensions, &block)
-
end
-
-
# Include the helper modules provided in Sinatra's request context.
-
1
def self.helpers(*extensions, &block)
-
Delegator.target.helpers(*extensions, &block)
-
end
-
-
# Use the middleware for classic applications.
-
1
def self.use(*args, &block)
-
Delegator.target.use(*args, &block)
-
end
-
end
-
# This can be removed once rack/rack@2fd9df71 is released
-
1
module Sinatra
-
1
module Ext
-
1
def self.get_handler(str)
-
begin
-
::Object.const_get("Object", false)
-
def self._const_get(str, inherit = true)
-
Rack::Handler.const_get(str, inherit)
-
end
-
rescue
-
def self._const_get(str, inherit = true)
-
Rack::Handler.const_get(str)
-
end
-
end
-
end
-
end
-
end
-
1
begin
-
1
require 'rack/show_exceptions'
-
rescue LoadError
-
1
require 'rack/showexceptions'
-
end
-
-
1
module Sinatra
-
# Sinatra::ShowExceptions catches all exceptions raised from the app it
-
# wraps. It shows a useful backtrace with the sourcefile and clickable
-
# context, the whole Rack environment and the request data.
-
#
-
# Be careful when you use this on public-facing sites as it could reveal
-
# information helpful to attackers.
-
1
class ShowExceptions < Rack::ShowExceptions
-
1
@@eats_errors = Object.new
-
1
def @@eats_errors.flush(*) end
-
1
def @@eats_errors.puts(*) end
-
-
1
def initialize(app)
-
@app = app
-
@template = ERB.new(TEMPLATE)
-
end
-
-
1
def call(env)
-
@app.call(env)
-
rescue Exception => e
-
errors, env["rack.errors"] = env["rack.errors"], @@eats_errors
-
-
if prefers_plain_text?(env)
-
content_type = "text/plain"
-
exception = dump_exception(e)
-
else
-
content_type = "text/html"
-
exception = pretty(env, e)
-
end
-
-
env["rack.errors"] = errors
-
-
# Post 893a2c50 in rack/rack, the #pretty method above, implemented in
-
# Rack::ShowExceptions, returns a String instead of an array.
-
body = Array(exception)
-
-
[
-
500,
-
{
-
"Content-Type" => content_type,
-
"Content-Length" => Rack::Utils.bytesize(body.join).to_s
-
},
-
body
-
]
-
end
-
-
1
private
-
-
1
def prefers_plain_text?(env)
-
!(Request.new(env).preferred_type("text/plain","text/html") == "text/html") &&
-
[/curl/].index{|item| item =~ env["HTTP_USER_AGENT"]}
-
end
-
-
1
def frame_class(frame)
-
if frame.filename =~ /lib\/sinatra.*\.rb/
-
"framework"
-
elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) ||
-
frame.filename =~ /\/bin\/(\w+)$/
-
"system"
-
else
-
"app"
-
end
-
end
-
-
1
TEMPLATE = <<-HTML # :nodoc:
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-
<title><%=h exception.class %> at <%=h path %></title>
-
-
<script type="text/javascript">
-
//<!--
-
function toggle(id) {
-
var pre = document.getElementById("pre-" + id);
-
var post = document.getElementById("post-" + id);
-
var context = document.getElementById("context-" + id);
-
-
if (pre.style.display == 'block') {
-
pre.style.display = 'none';
-
post.style.display = 'none';
-
context.style.background = "none";
-
} else {
-
pre.style.display = 'block';
-
post.style.display = 'block';
-
context.style.background = "#fffed9";
-
}
-
}
-
-
function toggleBacktrace(){
-
var bt = document.getElementById("backtrace");
-
var toggler = document.getElementById("expando");
-
-
if (bt.className == 'condensed') {
-
bt.className = 'expanded';
-
toggler.innerHTML = "(condense)";
-
} else {
-
bt.className = 'condensed';
-
toggler.innerHTML = "(expand)";
-
}
-
}
-
//-->
-
</script>
-
-
<style type="text/css" media="screen">
-
* {margin: 0; padding: 0; border: 0; outline: 0;}
-
div.clear {clear: both;}
-
body {background: #EEEEEE; margin: 0; padding: 0;
-
font-family: 'Lucida Grande', 'Lucida Sans Unicode',
-
'Garuda';}
-
code {font-family: 'Lucida Console', monospace;
-
font-size: 12px;}
-
li {height: 18px;}
-
ul {list-style: none; margin: 0; padding: 0;}
-
ol:hover {cursor: pointer;}
-
ol li {white-space: pre;}
-
#explanation {font-size: 12px; color: #666666;
-
margin: 20px 0 0 100px;}
-
/* WRAP */
-
#wrap {width: 1000px; background: #FFFFFF; margin: 0 auto;
-
padding: 30px 50px 20px 50px;
-
border-left: 1px solid #DDDDDD;
-
border-right: 1px solid #DDDDDD;}
-
/* HEADER */
-
#header {margin: 0 auto 25px auto;}
-
#header img {float: left;}
-
#header #summary {float: left; margin: 12px 0 0 20px; width:660px;
-
font-family: 'Lucida Grande', 'Lucida Sans Unicode';}
-
h1 {margin: 0; font-size: 36px; color: #981919;}
-
h2 {margin: 0; font-size: 22px; color: #333333;}
-
#header ul {margin: 0; font-size: 12px; color: #666666;}
-
#header ul li strong{color: #444444;}
-
#header ul li {display: inline; padding: 0 10px;}
-
#header ul li.first {padding-left: 0;}
-
#header ul li.last {border: 0; padding-right: 0;}
-
/* BODY */
-
#backtrace,
-
#get,
-
#post,
-
#cookies,
-
#rack {width: 980px; margin: 0 auto 10px auto;}
-
p#nav {float: right; font-size: 14px;}
-
/* BACKTRACE */
-
a#expando {float: left; padding-left: 5px; color: #666666;
-
font-size: 14px; text-decoration: none; cursor: pointer;}
-
a#expando:hover {text-decoration: underline;}
-
h3 {float: left; width: 100px; margin-bottom: 10px;
-
color: #981919; font-size: 14px; font-weight: bold;}
-
#nav a {color: #666666; text-decoration: none; padding: 0 5px;}
-
#backtrace li.frame-info {background: #f7f7f7; padding-left: 10px;
-
font-size: 12px; color: #333333;}
-
#backtrace ul {list-style-position: outside; border: 1px solid #E9E9E9;
-
border-bottom: 0;}
-
#backtrace ol {width: 920px; margin-left: 50px;
-
font: 10px 'Lucida Console', monospace; color: #666666;}
-
#backtrace ol li {border: 0; border-left: 1px solid #E9E9E9;
-
padding: 2px 0;}
-
#backtrace ol code {font-size: 10px; color: #555555; padding-left: 5px;}
-
#backtrace-ul li {border-bottom: 1px solid #E9E9E9; height: auto;
-
padding: 3px 0;}
-
#backtrace-ul .code {padding: 6px 0 4px 0;}
-
#backtrace.condensed .system,
-
#backtrace.condensed .framework {display:none;}
-
/* REQUEST DATA */
-
p.no-data {padding-top: 2px; font-size: 12px; color: #666666;}
-
table.req {width: 980px; text-align: left; font-size: 12px;
-
color: #666666; padding: 0; border-spacing: 0;
-
border: 1px solid #EEEEEE; border-bottom: 0;
-
border-left: 0;
-
clear:both}
-
table.req tr th {padding: 2px 10px; font-weight: bold;
-
background: #F7F7F7; border-bottom: 1px solid #EEEEEE;
-
border-left: 1px solid #EEEEEE;}
-
table.req tr td {padding: 2px 20px 2px 10px;
-
border-bottom: 1px solid #EEEEEE;
-
border-left: 1px solid #EEEEEE;}
-
/* HIDE PRE/POST CODE AT START */
-
.pre-context,
-
.post-context {display: none;}
-
-
table td.code {width:750px}
-
table td.code div {width:750px;overflow:hidden}
-
</style>
-
</head>
-
<body>
-
<div id="wrap">
-
<div id="header">
-
<img src="<%= env['SCRIPT_NAME'] %>/__sinatra__/500.png" alt="application error" height="161" width="313" />
-
<div id="summary">
-
<h1><strong><%=h exception.class %></strong> at <strong><%=h path %>
-
</strong></h1>
-
<h2><%=h exception.message %></h2>
-
<ul>
-
<li class="first"><strong>file:</strong> <code>
-
<%=h frames.first.filename.split("/").last %></code></li>
-
<li><strong>location:</strong> <code><%=h frames.first.function %>
-
</code></li>
-
<li class="last"><strong>line:
-
</strong> <%=h frames.first.lineno %></li>
-
</ul>
-
</div>
-
<div class="clear"></div>
-
</div>
-
-
<div id="backtrace" class='condensed'>
-
<h3>BACKTRACE</h3>
-
<p><a href="#" id="expando"
-
onclick="toggleBacktrace(); return false">(expand)</a></p>
-
<p id="nav"><strong>JUMP TO:</strong>
-
<a href="#get-info">GET</a>
-
<a href="#post-info">POST</a>
-
<a href="#cookie-info">COOKIES</a>
-
<a href="#env-info">ENV</a>
-
</p>
-
<div class="clear"></div>
-
-
<ul id="backtrace-ul">
-
-
<% id = 1 %>
-
<% frames.each do |frame| %>
-
<% if frame.context_line && frame.context_line != "#" %>
-
-
<li class="frame-info <%= frame_class(frame) %>">
-
<code><%=h frame.filename %></code> in
-
<code><strong><%=h frame.function %></strong></code>
-
</li>
-
-
<li class="code <%= frame_class(frame) %>">
-
<% if frame.pre_context %>
-
<ol start="<%=h frame.pre_context_lineno + 1 %>"
-
class="pre-context" id="pre-<%= id %>"
-
onclick="toggle(<%= id %>);">
-
<% frame.pre_context.each do |line| %>
-
<li class="pre-context-line"><code><%=h line %></code></li>
-
<% end %>
-
</ol>
-
<% end %>
-
-
<ol start="<%= frame.lineno %>" class="context" id="<%= id %>"
-
onclick="toggle(<%= id %>);">
-
<li class="context-line" id="context-<%= id %>"><code><%=
-
h frame.context_line %></code></li>
-
</ol>
-
-
<% if frame.post_context %>
-
<ol start="<%=h frame.lineno + 1 %>" class="post-context"
-
id="post-<%= id %>" onclick="toggle(<%= id %>);">
-
<% frame.post_context.each do |line| %>
-
<li class="post-context-line"><code><%=h line %></code></li>
-
<% end %>
-
</ol>
-
<% end %>
-
<div class="clear"></div>
-
</li>
-
-
<% end %>
-
-
<% id += 1 %>
-
<% end %>
-
-
</ul>
-
</div> <!-- /BACKTRACE -->
-
-
<div id="get">
-
<h3 id="get-info">GET</h3>
-
<% if req.GET and not req.GET.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No GET data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /GET -->
-
-
<div id="post">
-
<h3 id="post-info">POST</h3>
-
<% if req.POST and not req.POST.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No POST data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /POST -->
-
-
<div id="cookies">
-
<h3 id="cookie-info">COOKIES</h3>
-
<% unless req.cookies.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.cookies.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No cookie data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /COOKIES -->
-
-
<div id="rack">
-
<h3 id="env-info">Rack ENV</h3>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<div class="clear"></div>
-
</div> <!-- /RACK ENV -->
-
-
<p id="explanation">You're seeing this error because you have
-
enabled the <code>show_exceptions</code> setting.</p>
-
</div> <!-- /WRAP -->
-
</body>
-
</html>
-
HTML
-
end
-
end
-
1
module Sinatra
-
1
VERSION = '1.4.8'
-
end
-
1
require 'sinatra/base'
-
1
require 'sinatra/flash/storage'
-
1
require 'sinatra/flash/style'
-
-
-
1
module Sinatra
-
1
module Flash
-
-
1
def self.registered(app)
-
2
app.helpers Flash::Storage
-
2
app.helpers Flash::Style
-
-
# This callback rotates any flash structure we referenced, placing the 'next' hash into the session
-
# for the next request.
-
75
app.after {@flash.each{|key, flash| session[key] = @flash[key].next} if @flash}
-
end
-
-
end
-
-
1
register Flash
-
end
-
1
require 'delegate'
-
-
1
module Sinatra
-
1
module Flash
-
-
# A subclass of Hash that "remembers forward" by exactly one action.
-
# Tastes just like the API of Rails's ActionController::Flash::FlashHash, but with fewer calories.
-
1
class FlashHash < DelegateClass(Hash)
-
1
attr_reader :now, :next
-
-
# Builds a new FlashHash. It takes the hash for this action's values as an initialization variable.
-
1
def initialize(session)
-
2
@now = session || Hash.new
-
2
@next = Hash.new
-
2
super(@now)
-
end
-
-
# We assign to the _next_ hash, but retrieve values from the _now_ hash. Freaky, huh?
-
1
def []=(key, value)
-
self.next[key] = value
-
end
-
-
# Swaps out the current flash for the future flash, then returns it.
-
1
def sweep
-
@now.replace(@next)
-
@next = Hash.new
-
@now
-
end
-
-
# Keep all or one of the current values for next time.
-
1
def keep(key=nil)
-
2
if key
-
@next[key] = @now[key]
-
else
-
2
@next.merge!(@now)
-
end
-
end
-
-
# Tosses any values or one value before next time.
-
1
def discard(key=nil)
-
if key
-
@next.delete(key)
-
else
-
@next = Hash.new
-
end
-
end
-
end
-
end
-
end
-
1
require 'sinatra/flash/hash'
-
-
1
module Sinatra
-
1
module Flash
-
1
module Storage
-
-
# The main Sinatra helper for accessing the flash. You can have multiple flash collections (e.g.,
-
# for different apps in your Rack stack) by passing a symbol to it.
-
#
-
# @param [optional, String, Symbol] key Specifies which key in the session contains the hash
-
# you want to reference. Defaults to ':flash'. If there is no session or the key is not found,
-
# an empty hash is used. Note that this is only used in the case of multiple flash _collections_,
-
# which is rarer than multiple flash messages.
-
#
-
# @return [FlashHash] Assign to this like any other hash.
-
1
def flash(key=:flash)
-
2
@flash ||= {}
-
2
@flash[key.to_sym] ||= FlashHash.new((session ? session[key.to_sym] : {}))
-
end
-
-
end
-
end
-
end
-
1
module Sinatra
-
1
module Flash
-
1
module Style
-
-
# A view helper for rendering flash messages to HTML with reasonable CSS structure. Handles
-
# multiple flash messages in one request. Wraps them in a <div> tag with id #flash containing
-
# a <div> for each message with classes of .flash and the message type. E.g.:
-
#
-
# @example
-
# <div id='flash'>
-
# <div class='flash info'>Today is Tuesday, April 27th.</div>
-
# <div class='flash warning'>Missiles are headed to destroy the Earth!</div>
-
# </div>
-
#
-
# It is your responsibility to style these classes the way you want in your stylesheets.
-
#
-
# @param[optional, String, Symbol] key Specifies which flash collection you want to display.
-
# If you use this, the collection key will be appended to the top-level div id (e.g.,
-
# 'flash_login' if you pass a key of :login).
-
#
-
# @return [String] Styled HTML if the flash contains messages, or an empty string if it's empty.
-
1
def styled_flash(key=:flash)
-
return "" if flash(key).empty?
-
id = (key == :flash ? "flash" : "flash_#{key}")
-
messages = flash(key).collect {|message| " <div class='flash #{message[0]}'>#{message[1]}</div>\n"}
-
"<div id='#{id}'>\n" + messages.join + "</div>"
-
end
-
-
end
-
end
-
end
-
-
-
# encoding: UTF-8
-
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
1
module Partial
-
-
# This is here to make testing the private code easier while not including it in the helpers.
-
1
module Private
-
-
# This gets the path to the template, taking into account whether leading underscores are needed.
-
# @private
-
# param [String] partial_path
-
# param [true,false,nil] underscores Defaults to false
-
1
def self.partial_expand_path(partial_path, underscores=false)
-
underscores ||= false
-
dirs, base = File.dirname(partial_path), File.basename(partial_path)
-
base.insert(0, "_") if underscores
-
xs = dirs == "." ? [base] : [dirs, base]
-
File.join(xs).to_sym
-
end
-
-
# This takes the name of the local from the template's name, and corrects local by removing leading underscore if it's there.
-
# @private
-
# param [String] partial_path
-
1
def self.partial_local(partial_path)
-
partial_path = partial_path[1..-1] if partial_path.start_with? "_"
-
File.basename(partial_path).to_sym
-
end
-
end
-
-
-
1
module Helpers
-
# Renders a partial to a string.
-
#
-
# @param [#to_s] partial_name The partial to render.
-
# @param [Hash] options The options to render the partial with.
-
# @option options [Hash] :locals Local variables to render with
-
# @option options [Array] :collection Renders the template once per object in this array.
-
# @option options [Symbol] :template_engine The template engine to use. Haml by default.
-
# @option options [true,false] :underscores Set to true if you wish to follow the Rails convention of partial files having a leading underscore.
-
#
-
# @return [String] The rendered template contents.
-
#
-
# @example simply render a partial
-
# partial(:meta, :locals => {meta: meta})
-
# # => renders views/_meta.haml
-
#
-
# @example render a partial in a subfolder
-
# partial("meta/news", :locals => {news: [<News>]})
-
# # => renders views/meta/_news.haml
-
#
-
# @example render a collection of objects with one partial
-
# partial(:"meta/news", :collection => [<News>])
-
# # => renders views/meta/_news.haml once per item in :collection,
-
# with the local variable `news` being the current item in the iteration
-
1
def partial(partial_name, options={})
-
options.merge! :layout => false
-
partial_location = partial_name.to_s
-
engine = options.fetch(:template_engine, settings.partial_template_engine)
-
underscores = options.fetch(:underscores, settings.partial_underscores)
-
-
template = Private.partial_expand_path(partial_location, underscores)
-
-
if collection = options.delete(:collection)
-
member_local = Private.partial_local(partial_location)
-
-
locals = options.fetch(:locals, {})
-
-
collection.inject([]) do |buffer, member|
-
new_locals = {member_local => member}.merge(locals)
-
buffer << self.method(engine).call(template, options.merge(:locals => new_locals))
-
end.join("\n")
-
else
-
# TODO benchmark this and see if caching the method
-
# speeds things up
-
self.method(engine).call(template, options)
-
end
-
end
-
-
end # of Helpers
-
-
# This is here to allow configuration options to be set.
-
# @private
-
1
def self.registered(app)
-
2
app.helpers(Partial::Helpers)
-
-
# Configuration
-
2
app.set :partial_underscores, false
-
2
app.set :partial_template_engine, :haml
-
end
-
-
end # Partial
-
1
register(Sinatra::Partial)
-
end
-
-
-
1
require 'temple'
-
1
require 'slim/parser'
-
1
require 'slim/filter'
-
1
require 'slim/do_inserter'
-
1
require 'slim/end_inserter'
-
1
require 'slim/embedded'
-
1
require 'slim/interpolation'
-
1
require 'slim/controls'
-
1
require 'slim/splat/filter'
-
1
require 'slim/splat/builder'
-
1
require 'slim/code_attributes'
-
1
require 'slim/engine'
-
1
require 'slim/template'
-
1
require 'slim/version'
-
1
module Slim
-
# @api private
-
1
class CodeAttributes < Filter
-
1
define_options :merge_attrs
-
-
# Handle attributes expression `[:html, :attrs, *attrs]`
-
#
-
# @param [Array] attrs Array of temple expressions
-
# @return [Array] Compiled temple expression
-
1
def on_html_attrs(*attrs)
-
157
[:multi, *attrs.map {|a| compile(a) }]
-
end
-
-
# Handle attribute expression `[:html, :attr, name, value]`
-
#
-
# @param [String] name Attribute name
-
# @param [Array] value Value expression
-
# @return [Array] Compiled temple expression
-
1
def on_html_attr(name, value)
-
90
if value[0] == :slim && value[1] == :attrvalue && !options[:merge_attrs][name]
-
# We handle the attribute as a boolean attribute
-
escape, code = value[2], value[3]
-
case code
-
when 'true'
-
[:html, :attr, name, [:multi]]
-
when 'false', 'nil'
-
[:multi]
-
else
-
tmp = unique_name
-
[:multi,
-
[:code, "#{tmp} = #{code}"],
-
[:if, tmp,
-
[:if, "#{tmp} == true",
-
[:html, :attr, name, [:multi]],
-
[:html, :attr, name, [:escape, escape, [:dynamic, tmp]]]]]]
-
end
-
else
-
# Attribute with merging
-
90
@attr = name
-
90
super
-
end
-
end
-
-
# Handle attribute expression `[:slim, :attrvalue, escape, code]`
-
#
-
# @param [Boolean] escape Escape html
-
# @param [String] code Ruby code
-
# @return [Array] Compiled temple expression
-
1
def on_slim_attrvalue(escape, code)
-
# We perform attribute merging on Array values
-
if delimiter = options[:merge_attrs][@attr]
-
tmp = unique_name
-
[:multi,
-
[:code, "#{tmp} = #{code}"],
-
[:if, "Array === #{tmp}",
-
[:multi,
-
[:code, "#{tmp} = #{tmp}.flatten"],
-
[:code, "#{tmp}.map!(&:to_s)"],
-
[:code, "#{tmp}.reject!(&:empty?)"],
-
[:escape, escape, [:dynamic, "#{tmp}.join(#{delimiter.inspect})"]]],
-
[:escape, escape, [:dynamic, tmp]]]]
-
else
-
[:escape, escape, [:dynamic, code]]
-
end
-
end
-
end
-
end
-
1
module Slim
-
# @api private
-
1
class Controls < Filter
-
1
define_options :disable_capture
-
-
1
IF_RE = /\A(if|unless)\b|\bdo\s*(\|[^\|]*\|)?\s*$/
-
-
# Handle control expression `[:slim, :control, code, content]`
-
#
-
# @param [String] code Ruby code
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_slim_control(code, content)
-
3
[:multi,
-
[:code, code],
-
compile(content)]
-
end
-
-
# Handle output expression `[:slim, :output, escape, code, content]`
-
#
-
# @param [Boolean] escape Escape html
-
# @param [String] code Ruby code
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_slim_output(escape, code, content)
-
2
if code =~ IF_RE
-
tmp = unique_name
-
-
[:multi,
-
# Capture the result of the code in a variable. We can't do
-
# `[:dynamic, code]` because it's probably not a complete
-
# expression (which is a requirement for Temple).
-
[:block, "#{tmp} = #{code}",
-
-
# Capture the content of a block in a separate buffer. This means
-
# that `yield` will not output the content to the current buffer,
-
# but rather return the output.
-
#
-
# The capturing can be disabled with the option :disable_capture.
-
# Output code in the block writes directly to the output buffer then.
-
# Rails handles this by replacing the output buffer for helpers.
-
options[:disable_capture] ? compile(content) : [:capture, unique_name, compile(content)]],
-
-
# Output the content.
-
[:escape, escape, [:dynamic, tmp]]]
-
else
-
2
[:multi, [:escape, escape, [:dynamic, code]], content]
-
end
-
end
-
-
# Handle text expression `[:slim, :text, type, content]`
-
#
-
# @param [Symbol] type Text type
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_slim_text(type, content)
-
18
compile(content)
-
end
-
end
-
end
-
1
module Slim
-
# In Slim you don't need the do keyword sometimes. This
-
# filter adds the missing keyword.
-
#
-
# - 10.times
-
# | Hello
-
#
-
# @api private
-
1
class DoInserter < Filter
-
1
BLOCK_REGEX = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^\|]*\|\s*)?\Z/
-
-
# Handle control expression `[:slim, :control, code, content]`
-
#
-
# @param [String] code Ruby code
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_slim_control(code, content)
-
3
code = code + ' do' unless code =~ BLOCK_REGEX || empty_exp?(content)
-
3
[:slim, :control, code, compile(content)]
-
end
-
-
# Handle output expression `[:slim, :output, escape, code, content]`
-
#
-
# @param [Boolean] escape Escape html
-
# @param [String] code Ruby code
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_slim_output(escape, code, content)
-
2
code = code + ' do' unless code =~ BLOCK_REGEX || empty_exp?(content)
-
2
[:slim, :output, escape, code, compile(content)]
-
end
-
end
-
end
-
1
module Slim
-
# @api private
-
1
class TextCollector < Filter
-
1
def call(exp)
-
@collected = ''
-
super(exp)
-
@collected
-
end
-
-
1
def on_slim_interpolate(text)
-
@collected << text
-
nil
-
end
-
end
-
-
# @api private
-
1
class NewlineCollector < Filter
-
1
def call(exp)
-
@collected = [:multi]
-
super(exp)
-
@collected
-
end
-
-
1
def on_newline
-
@collected << [:newline]
-
nil
-
end
-
end
-
-
# @api private
-
1
class OutputProtector < Filter
-
1
def call(exp)
-
@protect, @collected, @tag = [], '', "%#{object_id.abs.to_s(36)}%"
-
super(exp)
-
@collected
-
end
-
-
1
def on_static(text)
-
@collected << text
-
nil
-
end
-
-
1
def on_slim_output(escape, text, content)
-
@collected << @tag
-
@protect << [:slim, :output, escape, text, content]
-
nil
-
end
-
-
1
def unprotect(text)
-
block = [:multi]
-
while text =~ /#{@tag}/
-
block << [:static, $`]
-
block << @protect.shift
-
text = $'
-
end
-
block << [:static, text]
-
end
-
end
-
-
# Temple filter which processes embedded engines
-
# @api private
-
1
class Embedded < Filter
-
1
@engines = {}
-
-
1
class << self
-
1
attr_reader :engines
-
-
# Register embedded engine
-
#
-
# @param [String] name Name of the engine
-
# @param [Class] klass Engine class
-
# @param option_filter List of options to pass to engine.
-
# Last argument can be default option hash.
-
1
def register(name, klass, *option_filter)
-
20
name = name.to_sym
-
20
local_options = option_filter.last.respond_to?(:to_hash) ? option_filter.pop.to_hash : {}
-
20
define_options(name, *option_filter)
-
20
klass.define_options(name)
-
20
engines[name.to_sym] = proc do |options|
-
klass.new({}.update(options).delete_if {|k,v| !option_filter.include?(k) && k != name }.update(local_options))
-
end
-
end
-
-
1
def create(name, options)
-
constructor = engines[name] || raise(Temple::FilterError, "Embedded engine #{name} not found")
-
constructor.call(options)
-
end
-
end
-
-
1
define_options :enable_engines, :disable_engines
-
-
1
def initialize(opts = {})
-
5
super
-
5
@engines = {}
-
5
@enabled = normalize_engine_list(options[:enable_engines])
-
5
@disabled = normalize_engine_list(options[:disable_engines])
-
end
-
-
1
def on_slim_embedded(name, body)
-
name = name.to_sym
-
raise(Temple::FilterError, "Embedded engine #{name} is disabled") unless enabled?(name)
-
@engines[name] ||= self.class.create(name, options)
-
@engines[name].on_slim_embedded(name, body)
-
end
-
-
1
def enabled?(name)
-
(!@enabled || @enabled.include?(name)) &&
-
(!@disabled || !@disabled.include?(name))
-
end
-
-
1
protected
-
-
1
def normalize_engine_list(list)
-
10
raise(ArgumentError, "Option :enable_engines/:disable_engines must be String or Symbol list") unless !list || Array === list
-
10
list && list.map(&:to_sym)
-
end
-
-
1
class Engine < Filter
-
1
protected
-
-
1
def collect_text(body)
-
@text_collector ||= TextCollector.new
-
@text_collector.call(body)
-
end
-
-
1
def collect_newlines(body)
-
@newline_collector ||= NewlineCollector.new
-
@newline_collector.call(body)
-
end
-
end
-
-
# Basic tilt engine
-
1
class TiltEngine < Engine
-
1
def on_slim_embedded(engine, body)
-
tilt_engine = Tilt[engine] || raise(Temple::FilterError, "Tilt engine #{engine} is not available.")
-
tilt_options = options[engine.to_sym] || {}
-
[:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)]
-
end
-
-
1
protected
-
-
1
def tilt_render(tilt_engine, tilt_options, text)
-
[:static, tilt_engine.new(tilt_options) { text }.render]
-
end
-
end
-
-
# Sass engine which supports :pretty option
-
1
class SassEngine < TiltEngine
-
1
define_options :pretty
-
-
1
protected
-
-
1
def tilt_render(tilt_engine, tilt_options, text)
-
text = tilt_engine.new(tilt_options.merge(
-
style: options[:pretty] ? :expanded : :compressed,
-
cache: false)) { text }.render
-
text.chomp!
-
[:static, text]
-
end
-
end
-
-
# Tilt-based engine which is precompiled
-
1
class PrecompiledTiltEngine < TiltEngine
-
1
protected
-
-
1
def tilt_render(tilt_engine, tilt_options, text)
-
# HACK: Tilt::Engine#precompiled is protected
-
[:dynamic, tilt_engine.new(tilt_options) { text }.send(:precompiled, {}).first]
-
end
-
end
-
-
# Static template with interpolated ruby code
-
1
class InterpolateTiltEngine < TiltEngine
-
1
def collect_text(body)
-
output_protector.call(interpolation.call(body))
-
end
-
-
1
def tilt_render(tilt_engine, tilt_options, text)
-
output_protector.unprotect(tilt_engine.new(tilt_options) { text }.render)
-
end
-
-
1
private
-
-
1
def interpolation
-
@interpolation ||= Interpolation.new
-
end
-
-
1
def output_protector
-
@output_protector ||= OutputProtector.new
-
end
-
end
-
-
# ERB engine (uses the Temple ERB implementation)
-
1
class ERBEngine < Engine
-
1
def on_slim_embedded(engine, body)
-
[:multi, [:newline], erb_parser.call(collect_text(body))]
-
end
-
-
1
protected
-
-
1
def erb_parser
-
@erb_parser ||= Temple::ERB::Parser.new
-
end
-
end
-
-
# Tag wrapper engine
-
# Generates a html tag and wraps another engine (specified via :engine option)
-
1
class TagEngine < Engine
-
1
disable_option_validator!
-
-
1
def on_slim_embedded(engine, body)
-
if options[:engine]
-
opts = {}.update(options)
-
opts.delete(:engine)
-
opts.delete(:tag)
-
opts.delete(:attributes)
-
@engine ||= options[:engine].new(opts)
-
body = @engine.on_slim_embedded(engine, body)
-
end
-
[:html, :tag, options[:tag], [:html, :attrs, *options[:attributes].map {|k, v| [:html, :attr, k, [:static, v]] }], body]
-
end
-
end
-
-
# Javascript wrapper engine.
-
# Like TagEngine, but can wrap content in html comment or cdata.
-
1
class JavaScriptEngine < TagEngine
-
1
disable_option_validator!
-
-
1
set_options tag: :script, attributes: {}
-
-
1
def on_slim_embedded(engine, body)
-
super(engine, [:html, :js, body])
-
end
-
end
-
-
# Embeds ruby code
-
1
class RubyEngine < Engine
-
1
def on_slim_embedded(engine, body)
-
[:multi, [:newline], [:code, collect_text(body)]]
-
end
-
end
-
-
# These engines are executed at compile time, embedded ruby is interpolated
-
1
register :asciidoc, InterpolateTiltEngine
-
1
register :markdown, InterpolateTiltEngine
-
1
register :textile, InterpolateTiltEngine
-
1
register :rdoc, InterpolateTiltEngine
-
1
register :creole, InterpolateTiltEngine
-
1
register :wiki, InterpolateTiltEngine
-
1
register :mediawiki, InterpolateTiltEngine
-
1
register :org, InterpolateTiltEngine
-
-
# These engines are executed at compile time
-
1
register :coffee, JavaScriptEngine, engine: TiltEngine
-
1
register :opal, JavaScriptEngine, engine: TiltEngine
-
1
register :less, TagEngine, tag: :style, attributes: { type: 'text/css' }, engine: TiltEngine
-
1
register :styl, TagEngine, tag: :style, attributes: { type: 'text/css' }, engine: TiltEngine
-
1
register :sass, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
-
1
register :scss, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
-
-
# These engines are precompiled, code is embedded
-
1
register :erb, ERBEngine
-
1
register :nokogiri, PrecompiledTiltEngine
-
1
register :builder, PrecompiledTiltEngine
-
-
# Embedded javascript/css
-
1
register :javascript, JavaScriptEngine
-
1
register :css, TagEngine, tag: :style, attributes: { type: 'text/css' }
-
-
# Embedded ruby code
-
1
register :ruby, RubyEngine
-
end
-
end
-
1
module Slim
-
# In Slim you don't need to close any blocks:
-
#
-
# - if Slim.awesome?
-
# | But of course it is!
-
#
-
# However, the parser is not smart enough (and that's a good thing) to
-
# automatically insert end's where they are needed. Luckily, this filter
-
# does *exactly* that (and it does it well!)
-
#
-
# @api private
-
1
class EndInserter < Filter
-
1
IF_RE = /\A(if|begin|unless|else|elsif|when|rescue|ensure)\b|\bdo\s*(\|[^\|]*\|)?\s*$/
-
1
ELSE_RE = /\A(else|elsif|when|rescue|ensure)\b/
-
1
END_RE = /\Aend\b/
-
-
# Handle multi expression `[:multi, *exps]`
-
#
-
# @return [Array] Corrected Temple expression with ends inserted
-
1
def on_multi(*exps)
-
165
result = [:multi]
-
# This variable is true if the previous line was
-
# (1) a control code and (2) contained indented content.
-
165
prev_indent = false
-
-
165
exps.each do |exp|
-
257
if control?(exp)
-
3
raise(Temple::FilterError, 'Explicit end statements are forbidden') if exp[2] =~ END_RE
-
-
# Two control code in a row. If this one is *not*
-
# an else block, we should close the previous one.
-
3
append_end(result) if prev_indent && exp[2] !~ ELSE_RE
-
-
# Indent if the control code starts a block.
-
3
prev_indent = exp[2] =~ IF_RE
-
254
elsif exp[0] != :newline && prev_indent
-
# This is *not* a control code, so we should close the previous one.
-
# Ignores newlines because they will be inserted after each line.
-
append_end(result)
-
prev_indent = false
-
end
-
-
257
result << compile(exp)
-
end
-
-
# The last line can be a control code too.
-
165
prev_indent ? append_end(result) : result
-
end
-
-
1
private
-
-
# Appends an end
-
1
def append_end(result)
-
2
result << [:code, 'end']
-
end
-
-
# Checks if an expression is a Slim control code
-
1
def control?(exp)
-
257
exp[0] == :slim && exp[1] == :control
-
end
-
end
-
end
-
# The Slim module contains all Slim related classes (e.g. Engine, Parser).
-
# Plugins might also reside within the Slim module (e.g. Include, Smart).
-
# @api public
-
1
module Slim
-
# Slim engine which transforms slim code to executable ruby code
-
# @api public
-
1
class Engine < Temple::Engine
-
# This overwrites some Temple default options or sets default options for Slim specific filters.
-
# It is recommended to set the default settings only once in the code and avoid duplication. Only use
-
# `define_options` when you have to override some default settings.
-
1
define_options pretty: false,
-
sort_attrs: true,
-
format: :xhtml,
-
attr_quote: '"',
-
merge_attrs: {'class' => ' '},
-
generator: Temple::Generators::ArrayBuffer,
-
default_tag: 'div'
-
-
1
filter :Encoding
-
1
filter :RemoveBOM
-
1
use Slim::Parser
-
1
use Slim::Embedded
-
1
use Slim::Interpolation
-
1
use Slim::Splat::Filter
-
1
use Slim::DoInserter
-
1
use Slim::EndInserter
-
1
use Slim::Controls
-
1
html :AttributeSorter
-
1
html :AttributeMerger
-
1
use Slim::CodeAttributes
-
6
use(:AttributeRemover) { Temple::HTML::AttributeRemover.new(remove_empty_attrs: options[:merge_attrs].keys) }
-
1
html :Pretty
-
1
filter :Escapable
-
1
filter :ControlFlow
-
1
filter :MultiFlattener
-
1
filter :StaticMerger
-
6
use(:Generator) { options[:generator] }
-
end
-
end
-
1
module Slim
-
# Base class for Temple filters used in Slim
-
#
-
# This base filter passes everything through and allows
-
# to override only some methods without affecting the rest
-
# of the expression.
-
#
-
# @api private
-
1
class Filter < Temple::HTML::Filter
-
# Pass-through handler
-
1
def on_slim_text(type, content)
-
108
[:slim, :text, type, compile(content)]
-
end
-
-
# Pass-through handler
-
1
def on_slim_embedded(type, content)
-
[:slim, :embedded, type, compile(content)]
-
end
-
-
# Pass-through handler
-
1
def on_slim_control(code, content)
-
15
[:slim, :control, code, compile(content)]
-
end
-
-
# Pass-through handler
-
1
def on_slim_output(escape, code, content)
-
7
[:slim, :output, escape, code, compile(content)]
-
end
-
end
-
end
-
1
require 'slim'
-
-
1
module Slim
-
# Handles inlined includes
-
#
-
# Slim files are compiled, non-Slim files are included as text with `#{interpolation}`
-
#
-
# @api private
-
1
class Include < Slim::Filter
-
1
define_options :file, include_dirs: [Dir.pwd, '.']
-
-
1
def on_html_tag(tag, attributes, content = nil)
-
69
return super if tag != 'include'
-
17
name = content.to_a.flatten.select {|s| String === s }.join
-
2
raise ArgumentError, 'Invalid include statement' unless attributes == [:html, :attrs] && !name.empty?
-
2
unless file = find_file(name)
-
name = "#{name}.slim" if name !~ /\.slim\Z/i
-
file = find_file(name)
-
end
-
2
raise Temple::FilterError, "'#{name}' not found in #{options[:include_dirs].join(':')}" unless file
-
2
content = File.read(file)
-
2
if file =~ /\.slim\Z/i
-
2
Thread.current[:slim_include_engine].call(content)
-
else
-
[:slim, :interpolate, content]
-
end
-
end
-
-
1
protected
-
-
1
def find_file(name)
-
2
current_dir = File.dirname(File.expand_path(options[:file]))
-
10
options[:include_dirs].map {|dir| File.expand_path(File.join(dir, name), current_dir) }.find {|file| File.file?(file) }
-
end
-
end
-
-
1
class Engine
-
1
after Slim::Parser, Slim::Include
-
1
after Slim::Include, :stop do |exp|
-
7
throw :stop, exp if Thread.current[:slim_include_level] > 1
-
5
exp
-
end
-
-
# @api private
-
1
alias call_without_include call
-
-
# @api private
-
1
def call(input)
-
7
Thread.current[:slim_include_engine] = self
-
7
Thread.current[:slim_include_level] ||= 0
-
7
Thread.current[:slim_include_level] += 1
-
14
catch(:stop) { call_without_include(input) }
-
ensure
-
7
Thread.current[:slim_include_engine] = nil if (Thread.current[:slim_include_level] -= 1) == 0
-
end
-
end
-
end
-
1
module Slim
-
# Perform interpolation of #{var_name} in the
-
# expressions `[:slim, :interpolate, string]`.
-
#
-
# @api private
-
1
class Interpolation < Filter
-
# Handle interpolate expression `[:slim, :interpolate, string]`
-
#
-
# @param [String] string Static interpolate
-
# @return [Array] Compiled temple expression
-
1
def on_slim_interpolate(string)
-
# Interpolate variables in text (#{variable}).
-
# Split the text into multiple dynamic and static parts.
-
86
block = [:multi]
-
begin
-
87
case string
-
when /\A\\#\{/
-
# Escaped interpolation
-
block << [:static, '#{']
-
string = $'
-
when /\A#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/
-
# Interpolation
-
1
string, code = $', $1
-
1
escape = code !~ /\A\{.*\}\Z/
-
1
block << [:slim, :output, escape, escape ? code : code[1..-2], [:multi]]
-
when /\A([#\\]?[^#\\]*([#\\][^\\#\{][^#\\]*)*)/
-
# Static text
-
86
block << [:static, $&]
-
86
string = $'
-
end
-
86
end until string.empty?
-
86
block
-
end
-
end
-
end
-
# coding: utf-8
-
1
module Slim
-
# Parses Slim code and transforms it to a Temple expression
-
# @api private
-
1
class Parser < Temple::Parser
-
1
define_options :file,
-
:default_tag,
-
tabsize: 4,
-
code_attr_delims: {
-
'(' => ')',
-
'[' => ']',
-
'{' => '}',
-
},
-
attr_list_delims: {
-
'(' => ')',
-
'[' => ']',
-
'{' => '}',
-
},
-
shortcut: {
-
'#' => { attr: 'id' },
-
'.' => { attr: 'class' }
-
}
-
-
1
class SyntaxError < StandardError
-
1
attr_reader :error, :file, :line, :lineno, :column
-
-
1
def initialize(error, file, line, lineno, column)
-
@error = error
-
@file = file || '(__TEMPLATE__)'
-
@line = line.to_s
-
@lineno = lineno
-
@column = column
-
end
-
-
1
def to_s
-
line = @line.lstrip
-
column = @column + line.size - @line.size
-
%{#{error}
-
#{file}, Line #{lineno}, Column #{@column}
-
#{line}
-
#{' ' * column}^
-
}
-
end
-
end
-
-
1
def initialize(opts = {})
-
5
super
-
5
@attr_list_delims = options[:attr_list_delims]
-
5
@code_attr_delims = options[:code_attr_delims]
-
5
tabsize = options[:tabsize]
-
5
if tabsize > 1
-
5
@tab_re = /\G((?: {#{tabsize}})*) {0,#{tabsize-1}}\t/
-
5
@tab = '\1' + ' ' * tabsize
-
else
-
@tab_re = "\t"
-
@tab = ' '
-
end
-
5
@tag_shortcut, @attr_shortcut, @additional_attrs = {}, {}, {}
-
5
options[:shortcut].each do |k,v|
-
30
raise ArgumentError, 'Shortcut requires :tag and/or :attr' unless (v[:attr] || v[:tag]) && (v.keys - [:attr, :tag, :additional_attrs]).empty?
-
30
@tag_shortcut[k] = v[:tag] || options[:default_tag]
-
30
if v.include?(:attr) || v.include?(:additional_attrs)
-
30
raise ArgumentError, 'You can only use special characters for attribute shortcuts' if k =~ /(\p{Word}|-)/
-
end
-
30
if v.include?(:attr)
-
30
@attr_shortcut[k] = [v[:attr]].flatten
-
end
-
30
if v.include?(:additional_attrs)
-
@additional_attrs[k] = v[:additional_attrs]
-
end
-
end
-
35
keys = Regexp.union @attr_shortcut.keys.sort_by {|k| -k.size }
-
5
@attr_shortcut_re = /\A(#{keys}+)((?:\p{Word}|-)*)/
-
35
keys = Regexp.union @tag_shortcut.keys.sort_by {|k| -k.size }
-
5
@tag_re = /\A(?:#{keys}|\*(?=[^\s]+)|(\p{Word}(?:\p{Word}|:|-)*\p{Word}|\p{Word}+))/
-
5
keys = Regexp.escape @code_attr_delims.keys.join
-
5
@code_attr_delims_re = /\A[#{keys}]/
-
5
keys = Regexp.escape @attr_list_delims.keys.join
-
5
@attr_list_delims_re = /\A\s*([#{keys}])/
-
5
@embedded_re = /\A(#{Regexp.union(Embedded.engines.keys.map(&:to_s))}):(\s*)/
-
5
keys = Regexp.escape ('"\'></='.split(//) + @attr_list_delims.flatten + @code_attr_delims.flatten).uniq.join
-
5
@attr_name = "\\A\\s*([^\0\s#{keys}]+)"
-
5
@quoted_attr_re = /#{@attr_name}\s*=(=?)\s*("|')/
-
5
@code_attr_re = /#{@attr_name}\s*=(=?)\s*/
-
end
-
-
# Compile string to Temple expression
-
#
-
# @param [String] str Slim code
-
# @return [Array] Temple expression representing the code]]
-
1
def call(str)
-
7
result = [:multi]
-
7
reset(str.split(/\r?\n/), [result])
-
-
7
parse_line while next_line
-
-
7
reset
-
7
result
-
end
-
-
1
protected
-
-
1
def reset(lines = nil, stacks = nil)
-
# Since you can indent however you like in Slim, we need to keep a list
-
# of how deeply indented you are. For instance, in a template like this:
-
#
-
# doctype # 0 spaces
-
# html # 0 spaces
-
# head # 1 space
-
# title # 4 spaces
-
#
-
# indents will then contain [0, 1, 4] (when it's processing the last line.)
-
#
-
# We uses this information to figure out how many steps we must "jump"
-
# out when we see an de-indented line.
-
14
@indents = []
-
-
# Whenever we want to output something, we'll *always* output it to the
-
# last stack in this array. So when there's a line that expects
-
# indentation, we simply push a new stack onto this array. When it
-
# processes the next line, the content will then be outputted into that
-
# stack.
-
14
@stacks = stacks
-
-
14
@lineno = 0
-
14
@lines = lines
-
14
@line = @orig_line = nil
-
end
-
-
1
def next_line
-
86
if @lines.empty?
-
7
@orig_line = @line = nil
-
else
-
79
@orig_line = @lines.shift
-
79
@lineno += 1
-
79
@line = @orig_line.dup
-
end
-
end
-
-
1
def get_indent(line)
-
# Figure out the indentation. Kinda ugly/slow way to support tabs,
-
# but remember that this is only done at parsing time.
-
94
line[/\A[ \t]*/].gsub(@tab_re, @tab).size
-
end
-
-
1
def parse_line
-
76
if @line =~ /\A\s*\Z/
-
1
@stacks.last << [:newline]
-
1
return
-
end
-
-
75
indent = get_indent(@line)
-
-
# Choose first indentation yourself
-
75
@indents << indent if @indents.empty?
-
-
# Remove the indentation
-
75
@line.lstrip!
-
-
# If there's more stacks than indents, it means that the previous
-
# line is expecting this line to be indented.
-
75
expecting_indentation = @stacks.size > @indents.size
-
-
75
if indent > @indents.last
-
# This line was actually indented, so we'll have to check if it was
-
# supposed to be indented or not.
-
20
syntax_error!('Unexpected indentation') unless expecting_indentation
-
-
20
@indents << indent
-
else
-
# This line was *not* indented more than the line before,
-
# so we'll just forget about the stack that the previous line pushed.
-
55
@stacks.pop if expecting_indentation
-
-
# This line was deindented.
-
# Now we're have to go through the all the indents and figure out
-
# how many levels we've deindented.
-
55
while indent < @indents.last && @indents.size > 1
-
9
@indents.pop
-
9
@stacks.pop
-
end
-
-
# This line's indentation happens lie "between" two other line's
-
# indentation:
-
#
-
# hello
-
# world
-
# this # <- This should not be possible!
-
55
syntax_error!('Malformed indentation') if indent != @indents.last
-
end
-
-
75
parse_line_indicators
-
end
-
-
1
def parse_line_indicators
-
75
case @line
-
when /\A\/!( ?)/
-
# HTML comment
-
@stacks.last << [:html, :comment, [:slim, :text, :verbatim, parse_text_block($', @indents.last + $1.size + 2)]]
-
when /\A\/\[\s*(.*?)\s*\]\s*\Z/
-
# HTML conditional comment
-
block = [:multi]
-
@stacks.last << [:html, :condcomment, $1, block]
-
@stacks << block
-
when /\A\//
-
# Slim comment
-
1
parse_comment_block
-
when /\A([\|'])( ?)/
-
# Found verbatim text block.
-
trailing_ws = $1 == "'"
-
@stacks.last << [:slim, :text, :verbatim, parse_text_block($', @indents.last + $2.size + 1)]
-
@stacks.last << [:static, ' '] if trailing_ws
-
when /\A</
-
# Inline html
-
block = [:multi]
-
@stacks.last << [:multi, [:slim, :interpolate, @line], block]
-
@stacks << block
-
when /\A-/
-
# Found a code block.
-
# We expect the line to be broken or the next line to be indented.
-
3
@line.slice!(0)
-
3
block = [:multi]
-
3
@stacks.last << [:slim, :control, parse_broken_line, block]
-
3
@stacks << block
-
when /\A=(=?)(['<>]*)/
-
# Found an output block.
-
# We expect the line to be broken or the next line to be indented.
-
1
@line = $'
-
1
trailing_ws = $2.include?('>'.freeze)
-
1
if $2.include?('\''.freeze)
-
deprecated_syntax '=\' for trailing whitespace is deprecated in favor of =>'
-
trailing_ws = true
-
end
-
1
block = [:multi]
-
1
@stacks.last << [:static, ' '] if $2.include?('<'.freeze)
-
1
@stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
-
1
@stacks.last << [:static, ' '] if trailing_ws
-
1
@stacks << block
-
when @embedded_re
-
# Embedded template detected. It is treated as block.
-
@stacks.last << [:slim, :embedded, $1, parse_text_block($', @orig_line.size - $'.size + $2.size)]
-
when /\Adoctype\b/
-
# Found doctype declaration
-
1
@stacks.last << [:html, :doctype, $'.strip]
-
when @tag_re
-
# Found a HTML tag.
-
69
@line = $' if $1
-
69
parse_tag($&)
-
else
-
unknown_line_indicator
-
end
-
75
@stacks.last << [:newline]
-
end
-
-
# Unknown line indicator found. Overwrite this method if
-
# you want to add line indicators to the Slim parser.
-
# The default implementation throws a syntax error.
-
1
def unknown_line_indicator
-
syntax_error! 'Unknown line indicator'
-
end
-
-
1
def parse_comment_block
-
1
while !@lines.empty? && (@lines.first =~ /\A\s*\Z/ || get_indent(@lines.first) > @indents.last)
-
2
next_line
-
2
@stacks.last << [:newline]
-
end
-
end
-
-
1
def parse_text_block(first_line = nil, text_indent = nil)
-
20
result = [:multi]
-
20
if !first_line || first_line.empty?
-
text_indent = nil
-
else
-
20
result << [:slim, :interpolate, first_line]
-
end
-
-
20
empty_lines = 0
-
20
until @lines.empty?
-
19
if @lines.first =~ /\A\s*\Z/
-
1
next_line
-
1
result << [:newline]
-
1
empty_lines += 1 if text_indent
-
else
-
18
indent = get_indent(@lines.first)
-
18
break if indent <= @indents.last
-
-
if empty_lines > 0
-
result << [:slim, :interpolate, "\n" * empty_lines]
-
empty_lines = 0
-
end
-
-
next_line
-
@line.lstrip!
-
-
# The text block lines must be at least indented
-
# as deep as the first line.
-
offset = text_indent ? indent - text_indent : 0
-
if offset < 0
-
text_indent += offset
-
offset = 0
-
end
-
result << [:newline] << [:slim, :interpolate, (text_indent ? "\n" : '') + (' ' * offset) + @line]
-
-
# The indentation of first line of the text block
-
# determines the text base indentation.
-
text_indent ||= indent
-
end
-
end
-
20
result
-
end
-
-
1
def parse_broken_line
-
4
broken_line = @line.strip
-
4
while broken_line =~ /[,\\]\Z/
-
expect_next_line
-
broken_line << "\n" << @line
-
end
-
4
broken_line
-
end
-
-
1
def parse_tag(tag)
-
69
if @tag_shortcut[tag]
-
21
@line.slice!(0, tag.size) unless @attr_shortcut[tag]
-
21
tag = @tag_shortcut[tag]
-
end
-
-
# Find any shortcut attributes
-
69
attributes = [:html, :attrs]
-
69
while @line =~ @attr_shortcut_re
-
# The class/id attribute is :static instead of :slim :interpolate,
-
# because we don't want text interpolation in .class or #id shortcut
-
22
syntax_error!('Illegal shortcut') unless shortcut = @attr_shortcut[$1]
-
44
shortcut.each {|a| attributes << [:html, :attr, a, [:static, $2]] }
-
22
if additional_attr_pairs = @additional_attrs[$1]
-
additional_attr_pairs.each do |k,v|
-
attributes << [:html, :attr, k.to_s, [:static, v]]
-
end
-
end
-
22
@line = $'
-
end
-
-
69
@line =~ /\A[<>']*/
-
69
@line = $'
-
69
trailing_ws = $&.include?('>'.freeze)
-
69
if $&.include?('\''.freeze)
-
deprecated_syntax 'tag\' for trailing whitespace is deprecated in favor of tag>'
-
trailing_ws = true
-
end
-
-
69
leading_ws = $&.include?('<'.freeze)
-
-
69
parse_attributes(attributes)
-
-
69
tag = [:html, :tag, tag, attributes]
-
-
69
@stacks.last << [:static, ' '] if leading_ws
-
69
@stacks.last << tag
-
69
@stacks.last << [:static, ' '] if trailing_ws
-
-
69
case @line
-
when /\A\s*:\s*/
-
# Block expansion
-
@line = $'
-
if @line =~ @embedded_re
-
tag << [:slim, :embedded, $1, parse_text_block($', @orig_line.size - @line.size + $2.size)]
-
else
-
(@line =~ @tag_re) || syntax_error!('Expected tag')
-
@line = $' if $1
-
content = [:multi]
-
tag << content
-
i = @stacks.size
-
@stacks << content
-
parse_tag($&)
-
@stacks.delete_at(i)
-
end
-
when /\A\s*=(=?)(['<>]*)/
-
# Handle output code
-
@line = $'
-
trailing_ws2 = $2.include?('>'.freeze)
-
if $2.include?('\''.freeze)
-
deprecated_syntax '=\' for trailing whitespace is deprecated in favor of =>'
-
trailing_ws2 = true
-
end
-
block = [:multi]
-
@stacks.last.insert(-2, [:static, ' ']) if !leading_ws && $2.include?('<'.freeze)
-
tag << [:slim, :output, $1 != '=', parse_broken_line, block]
-
@stacks.last << [:static, ' '] if !trailing_ws && trailing_ws2
-
@stacks << block
-
when /\A\s*\/\s*/
-
# Closed tag. Do nothing
-
@line = $'
-
syntax_error!('Unexpected text after closed tag') unless @line.empty?
-
when /\A\s*\Z/
-
# Empty content
-
49
content = [:multi]
-
49
tag << content
-
49
@stacks << content
-
when /\A ?/
-
# Text content
-
20
tag << [:slim, :text, :inline, parse_text_block($', @orig_line.size - $'.size)]
-
end
-
end
-
-
1
def parse_attributes(attributes)
-
# Check to see if there is a delimiter right after the tag name
-
69
delimiter = nil
-
69
if @line =~ @attr_list_delims_re
-
delimiter = @attr_list_delims[$1]
-
@line = $'
-
end
-
-
69
if delimiter
-
boolean_attr_re = /#{@attr_name}(?=(\s|#{Regexp.escape delimiter}|\Z))/
-
end_re = /\A\s*#{Regexp.escape delimiter}/
-
end
-
-
69
while true
-
137
case @line
-
when /\A\s*\*(?=[^\s]+)/
-
# Splat attribute
-
@line = $'
-
attributes << [:slim, :splat, parse_ruby_code(delimiter)]
-
when @quoted_attr_re
-
# Value is quoted (static)
-
68
@line = $'
-
attributes << [:html, :attr, $1,
-
68
[:escape, $2.empty?, [:slim, :interpolate, parse_quoted_attribute($3)]]]
-
when @code_attr_re
-
# Value is ruby code
-
@line = $'
-
name = $1
-
escape = $2.empty?
-
value = parse_ruby_code(delimiter)
-
syntax_error!('Invalid empty attribute') if value.empty?
-
attributes << [:html, :attr, name, [:slim, :attrvalue, escape, value]]
-
else
-
69
break unless delimiter
-
-
case @line
-
when boolean_attr_re
-
# Boolean attribute
-
@line = $'
-
attributes << [:html, :attr, $1, [:multi]]
-
when end_re
-
# Find ending delimiter
-
@line = $'
-
break
-
else
-
# Found something where an attribute should be
-
@line.lstrip!
-
syntax_error!('Expected attribute') unless @line.empty?
-
-
# Attributes span multiple lines
-
@stacks.last << [:newline]
-
syntax_error!("Expected closing delimiter #{delimiter}") if @lines.empty?
-
next_line
-
end
-
end
-
end
-
end
-
-
1
def parse_ruby_code(outer_delimiter)
-
code, count, delimiter, close_delimiter = '', 0, nil, nil
-
-
# Attribute ends with space or attribute delimiter
-
end_re = /\A[\s#{Regexp.escape outer_delimiter.to_s}]/
-
-
until @line.empty? || (count == 0 && @line =~ end_re)
-
if @line =~ /\A[,\\]\Z/
-
code << @line << "\n"
-
expect_next_line
-
else
-
if count > 0
-
if @line[0] == delimiter[0]
-
count += 1
-
elsif @line[0] == close_delimiter[0]
-
count -= 1
-
end
-
elsif @line =~ @code_attr_delims_re
-
count = 1
-
delimiter, close_delimiter = $&, @code_attr_delims[$&]
-
end
-
code << @line.slice!(0)
-
end
-
end
-
syntax_error!("Expected closing delimiter #{close_delimiter}") if count != 0
-
code
-
end
-
-
1
def parse_quoted_attribute(quote)
-
68
value, count = '', 0
-
-
68
until count == 0 && @line[0] == quote[0]
-
616
if @line =~ /\A(\\)?\Z/
-
value << ($1 ? ' ' : "\n")
-
expect_next_line
-
else
-
616
if @line[0] == ?{
-
count += 1
-
616
elsif @line[0] == ?}
-
count -= 1
-
end
-
616
value << @line.slice!(0)
-
end
-
end
-
-
68
@line.slice!(0)
-
68
value
-
end
-
-
# Helper for raising exceptions
-
1
def syntax_error!(message)
-
raise SyntaxError.new(message, options[:file], @orig_line, @lineno,
-
@orig_line && @line ? @orig_line.size - @line.size : 0)
-
rescue SyntaxError => ex
-
# HACK: Manipulate stacktrace for Rails and other frameworks
-
# to find the right file.
-
ex.backtrace.unshift "#{options[:file]}:#{@lineno}"
-
raise
-
end
-
-
1
def deprecated_syntax(message)
-
line = @orig_line.lstrip
-
column = (@orig_line && @line ? @orig_line.size - @line.size : 0) + line.size - @orig_line.size
-
warn %{Deprecated syntax: #{message}
-
#{options[:file]}, Line #{@lineno}, Column #{@column}
-
#{line}
-
#{' ' * column}^
-
}
-
end
-
-
1
def expect_next_line
-
next_line || syntax_error!('Unexpected end of file')
-
@line.strip!
-
end
-
end
-
end
-
1
module Slim
-
1
module Splat
-
# @api private
-
1
class Builder
-
1
def initialize(options)
-
@options = options
-
@attrs = {}
-
end
-
-
1
def code_attr(name, escape, value)
-
if delim = @options[:merge_attrs][name]
-
value = Array === value ? value.join(delim) : value.to_s
-
attr(name, escape_html(escape, value)) unless value.empty?
-
elsif @options[:hyphen_attrs].include?(name) && Hash === value
-
hyphen_attr(name, escape, value)
-
elsif value != false && value != nil
-
attr(name, escape_html(value != true && escape, value))
-
end
-
end
-
-
1
def splat_attrs(splat)
-
splat.each do |name, value|
-
code_attr(name.to_s, true, value)
-
end
-
end
-
-
1
def attr(name, value)
-
if @attrs[name]
-
if delim = @options[:merge_attrs][name]
-
@attrs[name] += delim + value.to_s
-
else
-
raise("Multiple #{name} attributes specified")
-
end
-
else
-
@attrs[name] = value
-
end
-
end
-
-
1
def build_tag(&block)
-
tag = @attrs.delete('tag').to_s
-
tag = @options[:default_tag] if tag.empty?
-
if block
-
# This is a bit of a hack to get a universal capturing.
-
#
-
# TODO: Add this as a helper somewhere to solve these capturing issues
-
# once and for all.
-
#
-
# If we have Slim capturing disabled and the scope defines the method `capture` (i.e. Rails)
-
# we use this method to capture the content.
-
#
-
# otherwise we just use normal Slim capturing (yield).
-
#
-
# See https://github.com/slim-template/slim/issues/591
-
# https://github.com/slim-template/slim#helpers-capturing-and-includes
-
#
-
content =
-
if @options[:disable_capture] && (scope = block.binding.eval('self')).respond_to?(:capture)
-
scope.capture(&block)
-
else
-
yield
-
end
-
"<#{tag}#{build_attrs}>#{content}</#{tag}>"
-
else
-
"<#{tag}#{build_attrs} />"
-
end
-
end
-
-
1
def build_attrs
-
attrs = @options[:sort_attrs] ? @attrs.sort_by(&:first) : @attrs
-
attrs.map do |k, v|
-
if v == true
-
if @options[:format] == :xhtml
-
" #{k}=#{@options[:attr_quote]}#{@options[:attr_quote]}"
-
else
-
" #{k}"
-
end
-
else
-
" #{k}=#{@options[:attr_quote]}#{v}#{@options[:attr_quote]}"
-
end
-
end.join
-
end
-
-
1
private
-
-
1
def hyphen_attr(name, escape, value)
-
if Hash === value
-
value.each do |n, v|
-
hyphen_attr("#{name}-#{n.to_s.gsub('_', '-')}", escape, v)
-
end
-
else
-
attr(name, escape_html(value != true && escape, value))
-
end
-
end
-
-
1
def escape_html(escape, value)
-
return value unless escape
-
@options[:use_html_safe] ? Temple::Utils.escape_html_safe(value) : Temple::Utils.escape_html(value)
-
end
-
end
-
end
-
end
-
1
module Slim
-
1
module Splat
-
# @api private
-
1
class Filter < ::Slim::Filter
-
1
define_options :merge_attrs, :attr_quote, :sort_attrs, :default_tag, :format, :disable_capture,
-
hyphen_attrs: %w(data aria), use_html_safe: ''.respond_to?(:html_safe?)
-
-
1
def call(exp)
-
5
@splat_options = nil
-
5
exp = compile(exp)
-
5
if @splat_options
-
opts = options.to_hash.reject {|k,v| !Filter.options.valid_key?(k) }.inspect
-
[:multi, [:code, "#{@splat_options} = #{opts}"], exp]
-
else
-
5
exp
-
end
-
end
-
-
# Handle tag expression `[:html, :tag, name, attrs, content]`
-
#
-
# @param [String] name Tag name
-
# @param [Array] attrs Temple expression
-
# @param [Array] content Temple expression
-
# @return [Array] Compiled temple expression
-
1
def on_html_tag(name, attrs, content = nil)
-
67
return super if name != '*'
-
builder, block = make_builder(attrs[2..-1])
-
if content
-
[:multi,
-
block,
-
[:slim, :output, false,
-
"#{builder}.build_tag #{empty_exp?(content) ? '{}' : 'do'}",
-
compile(content)]]
-
else
-
[:multi,
-
block,
-
[:dynamic, "#{builder}.build_tag"]]
-
end
-
end
-
-
# Handle attributes expression `[:html, :attrs, *attrs]`
-
#
-
# @param [Array] attrs Array of temple expressions
-
# @return [Array] Compiled temple expression
-
1
def on_html_attrs(*attrs)
-
157
if attrs.any? {|attr| splat?(attr) }
-
builder, block = make_builder(attrs)
-
[:multi,
-
block,
-
[:dynamic, "#{builder}.build_attrs"]]
-
else
-
67
super
-
end
-
end
-
-
1
protected
-
-
1
def splat?(attr)
-
# Splat attribute given
-
attr[0] == :slim && attr[1] == :splat ||
-
# Hyphenated attribute also needs splat handling
-
90
(attr[0] == :html && attr[1] == :attr && options[:hyphen_attrs].include?(attr[2]) &&
-
90
attr[3][0] == :slim && attr[3][1] == :attrvalue)
-
end
-
-
1
def make_builder(attrs)
-
@splat_options ||= unique_name
-
builder = unique_name
-
result = [:multi, [:code, "#{builder} = ::Slim::Splat::Builder.new(#{@splat_options})"]]
-
attrs.each do |attr|
-
result <<
-
if attr[0] == :html && attr[1] == :attr
-
if attr[3][0] == :slim && attr[3][1] == :attrvalue
-
[:code, "#{builder}.code_attr(#{attr[2].inspect}, #{attr[3][2]}, (#{attr[3][3]}))"]
-
else
-
tmp = unique_name
-
[:multi,
-
[:capture, tmp, compile(attr[3])],
-
[:code, "#{builder}.attr(#{attr[2].inspect}, #{tmp})"]]
-
end
-
elsif attr[0] == :slim && attr[1] == :splat
-
[:code, "#{builder}.splat_attrs((#{attr[2]}))"]
-
else
-
attr
-
end
-
end
-
return builder, result
-
end
-
end
-
end
-
end
-
1
module Slim
-
# Tilt template implementation for Slim
-
# @api public
-
1
Template = Temple::Templates::Tilt(Slim::Engine, register_as: :slim)
-
-
1
if defined?(::ActionView)
-
# Rails template implementation for Slim
-
# @api public
-
RailsTemplate = Temple::Templates::Rails(Slim::Engine,
-
register_as: :slim,
-
# Use rails-specific generator. This is necessary
-
# to support block capturing and streaming.
-
generator: Temple::Generators::RailsOutputBuffer,
-
# Disable the internal slim capturing.
-
# Rails takes care of the capturing by itself.
-
disable_capture: true,
-
streaming: true)
-
end
-
end
-
1
module Slim
-
# Slim version string
-
# @api public
-
1
VERSION = '3.0.7'
-
end
-
1
require 'temple/version'
-
-
1
module Temple
-
1
autoload :InvalidExpression, 'temple/exceptions'
-
1
autoload :FilterError, 'temple/exceptions'
-
1
autoload :Generator, 'temple/generator'
-
1
autoload :Parser, 'temple/parser'
-
1
autoload :Engine, 'temple/engine'
-
1
autoload :Utils, 'temple/utils'
-
1
autoload :Filter, 'temple/filter'
-
1
autoload :Templates, 'temple/templates'
-
1
autoload :Grammar, 'temple/grammar'
-
1
autoload :ImmutableMap, 'temple/map'
-
1
autoload :MutableMap, 'temple/map'
-
1
autoload :OptionMap, 'temple/map'
-
-
1
module Mixins
-
1
autoload :Dispatcher, 'temple/mixins/dispatcher'
-
1
autoload :CompiledDispatcher, 'temple/mixins/dispatcher'
-
1
autoload :EngineDSL, 'temple/mixins/engine_dsl'
-
1
autoload :GrammarDSL, 'temple/mixins/grammar_dsl'
-
1
autoload :Options, 'temple/mixins/options'
-
1
autoload :ClassOptions, 'temple/mixins/options'
-
1
autoload :Template, 'temple/mixins/template'
-
end
-
-
1
module ERB
-
1
autoload :Engine, 'temple/erb/engine'
-
1
autoload :Parser, 'temple/erb/parser'
-
1
autoload :Trimming, 'temple/erb/trimming'
-
1
autoload :Template, 'temple/erb/template'
-
end
-
-
1
module Generators
-
1
autoload :ERB, 'temple/generators/erb'
-
1
autoload :Array, 'temple/generators/array'
-
1
autoload :ArrayBuffer, 'temple/generators/array_buffer'
-
1
autoload :StringBuffer, 'temple/generators/string_buffer'
-
1
autoload :RailsOutputBuffer, 'temple/generators/rails_output_buffer'
-
end
-
-
1
module Filters
-
1
autoload :CodeMerger, 'temple/filters/code_merger'
-
1
autoload :ControlFlow, 'temple/filters/control_flow'
-
1
autoload :MultiFlattener, 'temple/filters/multi_flattener'
-
1
autoload :StaticAnalyzer, 'temple/filters/static_analyzer'
-
1
autoload :StaticMerger, 'temple/filters/static_merger'
-
1
autoload :StringSplitter, 'temple/filters/string_splitter'
-
1
autoload :DynamicInliner, 'temple/filters/dynamic_inliner'
-
1
autoload :Escapable, 'temple/filters/escapable'
-
1
autoload :Eraser, 'temple/filters/eraser'
-
1
autoload :Validator, 'temple/filters/validator'
-
1
autoload :Encoding, 'temple/filters/encoding'
-
1
autoload :RemoveBOM, 'temple/filters/remove_bom'
-
end
-
-
1
module HTML
-
1
autoload :Dispatcher, 'temple/html/dispatcher'
-
1
autoload :Filter, 'temple/html/filter'
-
1
autoload :Fast, 'temple/html/fast'
-
1
autoload :Pretty, 'temple/html/pretty'
-
1
autoload :AttributeMerger, 'temple/html/attribute_merger'
-
1
autoload :AttributeRemover, 'temple/html/attribute_remover'
-
1
autoload :AttributeSorter, 'temple/html/attribute_sorter'
-
end
-
end
-
1
module Temple
-
# An engine is simply a chain of compilers (that often includes a parser,
-
# some filters and a generator).
-
#
-
# class MyEngine < Temple::Engine
-
# # First run MyParser, passing the :strict option
-
# use MyParser, :strict
-
#
-
# # Then a custom filter
-
# use MyFilter
-
#
-
# # Then some general optimizations filters
-
# filter :MultiFlattener
-
# filter :StaticMerger
-
# filter :DynamicInliner
-
#
-
# # Finally the generator
-
# generator :ArrayBuffer, :buffer
-
# end
-
#
-
# class SpecialEngine < MyEngine
-
# append MyCodeOptimizer
-
# before :ArrayBuffer, Temple::Filters::Validator
-
# replace :ArrayBuffer, Temple::Generators::RailsOutputBuffer
-
# end
-
#
-
# engine = MyEngine.new(strict: "For MyParser")
-
# engine.call(something)
-
#
-
# @api public
-
1
class Engine
-
1
include Mixins::Options
-
1
include Mixins::EngineDSL
-
1
extend Mixins::EngineDSL
-
-
1
define_options :file, :streaming, :buffer, :save_buffer
-
-
1
attr_reader :chain
-
-
1
def self.chain
-
29
@chain ||= superclass.respond_to?(:chain) ? superclass.chain.dup : []
-
end
-
-
1
def initialize(opts = {})
-
5
super
-
5
@chain = self.class.chain.dup
-
end
-
-
1
def call(input)
-
122
call_chain.inject(input) {|m, e| e.call(m) }
-
end
-
-
1
protected
-
-
1
def chain_modified!
-
@call_chain = nil
-
end
-
-
1
def call_chain
-
@call_chain ||= @chain.map do |name, constructor|
-
105
f = constructor.call(self)
-
105
raise "Constructor #{name} must return callable object" if f && !f.respond_to?(:call)
-
105
f
-
7
end.compact
-
end
-
end
-
end
-
1
module Temple
-
# Temple base filter
-
# @api public
-
1
class Filter
-
1
include Utils
-
1
include Mixins::Dispatcher
-
1
include Mixins::Options
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Control flow filter which processes [:if, condition, yes-exp, no-exp]
-
# and [:block, code, content] expressions.
-
# This is useful for ruby code generation with lots of conditionals.
-
#
-
# @api public
-
1
class ControlFlow < Filter
-
1
def on_if(condition, yes, no = nil)
-
result = [:multi, [:code, "if #{condition}"], compile(yes)]
-
while no && no.first == :if
-
result << [:code, "elsif #{no[1]}"] << compile(no[2])
-
no = no[3]
-
end
-
result << [:code, 'else'] << compile(no) if no
-
result << [:code, 'end']
-
result
-
end
-
-
1
def on_case(arg, *cases)
-
result = [:multi, [:code, arg ? "case (#{arg})" : 'case']]
-
cases.map do |c|
-
condition, exp = c
-
result << [:code, condition == :else ? 'else' : "when #{condition}"] << compile(exp)
-
end
-
result << [:code, 'end']
-
result
-
end
-
-
1
def on_cond(*cases)
-
on_case(nil, *cases)
-
end
-
-
1
def on_block(code, exp)
-
[:multi,
-
[:code, code],
-
compile(exp),
-
[:code, 'end']]
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Try to encode input string
-
#
-
# @api public
-
1
class Encoding < Parser
-
1
define_options encoding: 'utf-8'
-
-
1
def call(s)
-
7
if options[:encoding] && s.respond_to?(:encoding)
-
7
old_enc = s.encoding
-
7
s = s.dup if s.frozen?
-
7
s.force_encoding(options[:encoding])
-
# Fall back to old encoding if new encoding is invalid
-
7
unless s.valid_encoding?
-
s.force_encoding(old_enc)
-
s.force_encoding(::Encoding::BINARY) unless s.valid_encoding?
-
end
-
end
-
7
s
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Escape dynamic or static expressions.
-
# This filter must be used after Temple::HTML::* and before the generators.
-
# It can be enclosed with Temple::Filters::DynamicInliner filters to
-
# reduce calls to Temple::Utils#escape_html.
-
#
-
# @api public
-
1
class Escapable < Filter
-
# Activate the usage of html_safe? if it is available (for Rails 3 for example)
-
1
define_options :escape_code,
-
:disable_escape,
-
use_html_safe: ''.respond_to?(:html_safe?)
-
-
1
def initialize(opts = {})
-
5
super
-
5
@escape_code = options[:escape_code] ||
-
"::Temple::Utils.escape_html#{options[:use_html_safe] ? '_safe' : ''}((%s))"
-
5
@escaper = eval("proc {|v| #{@escape_code % 'v'} }")
-
5
@escape = false
-
end
-
-
1
def on_escape(flag, exp)
-
70
old = @escape
-
70
@escape = flag && !options[:disable_escape]
-
70
compile(exp)
-
ensure
-
70
@escape = old
-
end
-
-
1
def on_static(value)
-
462
[:static, @escape ? @escaper[value] : value]
-
end
-
-
1
def on_dynamic(value)
-
2
[:dynamic, @escape ? @escape_code % value : value]
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Flattens nested multi expressions
-
#
-
# @api public
-
1
class MultiFlattener < Filter
-
1
def on_multi(*exps)
-
# If the multi contains a single element, just return the element
-
394
return compile(exps.first) if exps.size == 1
-
233
result = [:multi]
-
-
233
exps.each do |exp|
-
775
exp = compile(exp)
-
775
if exp.first == :multi
-
228
result.concat(exp[1..-1])
-
else
-
547
result << exp
-
end
-
end
-
-
233
result
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Remove BOM from input string
-
#
-
# @api public
-
1
class RemoveBOM < Parser
-
1
def call(s)
-
7
return s if s.encoding.name !~ /^UTF-(8|16|32)(BE|LE)?/
-
7
s.gsub(Regexp.new("\\A\uFEFF".encode(s.encoding.name)), ''.freeze)
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Filters
-
# Merges several statics into a single static. Example:
-
#
-
# [:multi,
-
# [:static, "Hello "],
-
# [:static, "World!"]]
-
#
-
# Compiles to:
-
#
-
# [:static, "Hello World!"]
-
#
-
# @api public
-
1
class StaticMerger < Filter
-
1
def on_multi(*exps)
-
5
result = [:multi]
-
5
text = nil
-
-
5
exps.each do |exp|
-
547
if exp.first == :static
-
462
if text
-
453
text << exp.last
-
else
-
9
text = exp.last.dup
-
9
result << [:static, text]
-
end
-
else
-
85
result << compile(exp)
-
85
text = nil unless exp.first == :newline
-
end
-
end
-
-
5
result.size == 2 ? result[1] : result
-
end
-
end
-
end
-
end
-
1
module Temple
-
# Abstract generator base class
-
# Generators should inherit this class and
-
# compile the Core Abstraction to ruby code.
-
#
-
# @api public
-
1
class Generator
-
1
include Utils
-
1
include Mixins::CompiledDispatcher
-
1
include Mixins::Options
-
-
1
define_options :save_buffer,
-
capture_generator: 'StringBuffer',
-
buffer: '_buf',
-
freeze_static: RUBY_VERSION >= '2.1'
-
-
1
def call(exp)
-
5
[preamble, compile(exp), postamble].flatten.compact.join('; ')
-
end
-
-
1
def preamble
-
5
[save_buffer, create_buffer]
-
end
-
-
1
def postamble
-
5
[return_buffer, restore_buffer]
-
end
-
-
1
def save_buffer
-
5
"begin; #{@original_buffer = unique_name} = #{buffer} if defined?(#{buffer})" if options[:save_buffer]
-
end
-
-
1
def restore_buffer
-
5
"ensure; #{buffer} = #{@original_buffer}; end" if options[:save_buffer]
-
end
-
-
1
def create_buffer
-
end
-
-
1
def return_buffer
-
'nil'
-
end
-
-
1
def on(*exp)
-
raise InvalidExpression, "Generator supports only core expressions - found #{exp.inspect}"
-
end
-
-
1
def on_multi(*exp)
-
99
exp.map {|e| compile(e) }.join('; '.freeze)
-
end
-
-
1
def on_newline
-
78
"\n"
-
end
-
-
1
def on_capture(name, exp)
-
capture_generator.new(buffer: name).call(exp)
-
end
-
-
1
def on_static(text)
-
9
concat(options[:freeze_static] ? "#{text.inspect}.freeze" : text.inspect)
-
end
-
-
1
def on_dynamic(code)
-
2
concat(code)
-
end
-
-
1
def on_code(code)
-
5
code
-
end
-
-
1
protected
-
-
1
def buffer
-
41
options[:buffer]
-
end
-
-
1
def capture_generator
-
@capture_generator ||= Class === options[:capture_generator] ?
-
options[:capture_generator] :
-
Generators.const_get(options[:capture_generator])
-
end
-
-
1
def concat(str)
-
11
"#{buffer} << (#{str})"
-
end
-
end
-
end
-
1
module Temple
-
1
module Generators
-
# Implements an array buffer.
-
#
-
# _buf = []
-
# _buf << "static"
-
# _buf << dynamic
-
# _buf
-
#
-
# @api public
-
1
class Array < Generator
-
1
def create_buffer
-
5
"#{buffer} = []"
-
end
-
-
1
def return_buffer
-
buffer
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Generators
-
# Just like Array, but calls #join on the array.
-
#
-
# _buf = []
-
# _buf << "static"
-
# _buf << dynamic
-
# _buf.join("")
-
#
-
# @api public
-
1
class ArrayBuffer < Array
-
1
def call(exp)
-
5
case exp.first
-
when :static
-
[save_buffer, "#{buffer} = #{exp.last.inspect}", restore_buffer].compact.join('; ')
-
when :dynamic
-
[save_buffer, "#{buffer} = (#{exp.last}).to_s", restore_buffer].compact.join('; ')
-
else
-
5
super
-
end
-
end
-
-
1
def return_buffer
-
5
freeze = options[:freeze_static] ? '.freeze' : ''
-
5
"#{buffer} = #{buffer}.join(\"\"#{freeze})"
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# This filter merges html attributes (e.g. used for id and class)
-
# @api public
-
1
class AttributeMerger < Filter
-
1
define_options merge_attrs: {'id' => '_', 'class' => ' '}
-
-
1
def on_html_attrs(*attrs)
-
67
values = {}
-
-
67
attrs.each do |_, _, name, value|
-
90
name = name.to_s
-
90
if values[name]
-
raise(FilterError, "Multiple #{name} attributes specified") unless options[:merge_attrs][name]
-
values[name] << value
-
else
-
90
values[name] = [value]
-
end
-
end
-
-
67
attrs = values.map do |name, value|
-
90
if (delimiter = options[:merge_attrs][name]) && value.size > 1
-
exp = [:multi]
-
if value.all? {|v| contains_nonempty_static?(v) }
-
exp << value.first
-
value[1..-1].each {|v| exp << [:static, delimiter] << v }
-
else
-
captures = unique_name
-
exp << [:code, "#{captures} = []"]
-
value.each_with_index {|v, i| exp << [:capture, "#{captures}[#{i}]", v] }
-
exp << [:dynamic, "#{captures}.reject(&:empty?).join(#{delimiter.inspect})"]
-
end
-
else
-
90
exp = value.first
-
end
-
90
[:html, :attr, name, exp]
-
end
-
67
[:html, :attrs, *attrs]
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# This filter removes empty attributes
-
# @api public
-
1
class AttributeRemover < Filter
-
1
define_options remove_empty_attrs: %w(id class)
-
-
1
def initialize(opts = {})
-
5
super
-
raise ArgumentError, "Option :remove_empty_attrs must be an Array of Strings" unless Array === options[:remove_empty_attrs] &&
-
10
options[:remove_empty_attrs].all? {|a| String === a }
-
end
-
-
1
def on_html_attrs(*attrs)
-
[:multi, *attrs.map {|attr| compile(attr) }]
-
end
-
-
1
def on_html_attr(name, value)
-
90
return super unless options[:remove_empty_attrs].include?(name.to_s)
-
-
9
if empty_exp?(value)
-
value
-
9
elsif contains_nonempty_static?(value)
-
9
[:html, :attr, name, value]
-
else
-
tmp = unique_name
-
[:multi,
-
[:capture, tmp, compile(value)],
-
[:if, "!#{tmp}.empty?",
-
[:html, :attr, name, [:dynamic, tmp]]]]
-
end
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# This filter sorts html attributes.
-
# @api public
-
1
class AttributeSorter < Filter
-
1
define_options sort_attrs: true
-
-
1
def call(exp)
-
5
options[:sort_attrs] ? super : exp
-
end
-
-
1
def on_html_attrs(*attrs)
-
67
n = 0 # Use n to make sort stable. This is important because the merger could be executed afterwards.
-
[:html, :attrs, *attrs.sort_by do |attr|
-
90
raise(InvalidExpression, 'Attribute is not a html attr') if attr[0] != :html || attr[1] != :attr
-
90
[attr[2].to_s, n += 1]
-
67
end]
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# @api private
-
1
module Dispatcher
-
1
def on_html_attrs(*attrs)
-
1099
[:html, :attrs, *attrs.map {|a| compile(a) }]
-
end
-
-
1
def on_html_attr(name, content)
-
801
[:html, :attr, name, compile(content)]
-
end
-
-
1
def on_html_comment(content)
-
[:html, :comment, compile(content)]
-
end
-
-
1
def on_html_condcomment(condition, content)
-
[:html, :condcomment, condition, compile(content)]
-
end
-
-
1
def on_html_js(content)
-
[:html, :js, compile(content)]
-
end
-
-
1
def on_html_tag(name, attrs, content = nil)
-
737
result = [:html, :tag, name, compile(attrs)]
-
737
content ? (result << compile(content)) : result
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# @api public
-
1
class Fast < Filter
-
1
DOCTYPES = {
-
xml: {
-
'1.1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
-
'5' => '<!DOCTYPE html>',
-
'html' => '<!DOCTYPE html>',
-
'strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
-
'frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
-
'mobile' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
-
'basic' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
-
'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
-
'svg' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
-
},
-
html: {
-
'5' => '<!DOCTYPE html>',
-
'html' => '<!DOCTYPE html>',
-
'strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
-
'frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
-
'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
-
}
-
}
-
1
DOCTYPES[:xhtml] = DOCTYPES[:xml]
-
1
DOCTYPES.freeze
-
-
# See http://www.w3.org/html/wg/drafts/html/master/single-page.html#void-elements
-
1
HTML_VOID_ELEMENTS = %w[area base br col embed hr img input keygen link menuitem meta param source track wbr]
-
-
1
define_options format: :xhtml,
-
attr_quote: '"',
-
autoclose: HTML_VOID_ELEMENTS,
-
js_wrapper: nil
-
-
1
def initialize(opts = {})
-
5
super
-
5
@format = options[:format]
-
5
unless [:xhtml, :html, :xml].include?(@format)
-
if @format == :html4 || @format == :html5
-
warn "Format #{@format.inspect} is deprecated, use :html"
-
@format = :html
-
else
-
raise ArgumentError, "Invalid format #{@format.inspect}"
-
end
-
end
-
5
wrapper = options[:js_wrapper]
-
5
wrapper = @format == :xml || @format == :xhtml ? :cdata : :comment if wrapper == :guess
-
5
@js_wrapper =
-
case wrapper
-
when :comment
-
[ "<!--\n", "\n//-->" ]
-
when :cdata
-
[ "\n//<![CDATA[\n", "\n//]]>\n" ]
-
when :both
-
[ "<!--\n//<![CDATA[\n", "\n//]]>\n//-->" ]
-
when nil
-
when Array
-
wrapper
-
else
-
raise ArgumentError, "Invalid JavaScript wrapper #{wrapper.inspect}"
-
end
-
end
-
-
1
def on_html_doctype(type)
-
1
type = type.to_s.downcase
-
-
1
if type =~ /^xml(\s+(.+))?$/
-
raise(FilterError, 'Invalid xml directive in html mode') if @format == :html
-
w = options[:attr_quote]
-
str = "<?xml version=#{w}1.0#{w} encoding=#{w}#{$2 || 'utf-8'}#{w} ?>"
-
else
-
1
str = DOCTYPES[@format][type] || raise(FilterError, "Invalid doctype #{type}")
-
end
-
-
1
[:static, str]
-
end
-
-
1
def on_html_comment(content)
-
[:multi,
-
[:static, '<!--'],
-
compile(content),
-
[:static, '-->']]
-
end
-
-
1
def on_html_condcomment(condition, content)
-
on_html_comment [:multi,
-
[:static, "[#{condition}]>"],
-
content,
-
[:static, '<![endif]']]
-
end
-
-
1
def on_html_tag(name, attrs, content = nil)
-
67
name = name.to_s
-
67
closed = !content || (empty_exp?(content) && (@format == :xml || options[:autoclose].include?(name)))
-
67
result = [:multi, [:static, "<#{name}"], compile(attrs)]
-
67
result << [:static, (closed && @format != :html ? ' /' : '') + '>']
-
67
result << compile(content) if content
-
67
result << [:static, "</#{name}>"] if !closed
-
67
result
-
end
-
-
1
def on_html_attrs(*attrs)
-
[:multi, *attrs.map {|attr| compile(attr) }]
-
end
-
-
1
def on_html_attr(name, value)
-
90
if @format == :html && empty_exp?(value)
-
[:static, " #{name}"]
-
else
-
90
[:multi,
-
[:static, " #{name}=#{options[:attr_quote]}"],
-
compile(value),
-
[:static, options[:attr_quote]]]
-
end
-
end
-
-
1
def on_html_js(content)
-
if @js_wrapper
-
[:multi,
-
[:static, @js_wrapper.first],
-
compile(content),
-
[:static, @js_wrapper.last]]
-
else
-
compile(content)
-
end
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# @api public
-
1
class Filter < Temple::Filter
-
1
include Dispatcher
-
-
1
def contains_nonempty_static?(exp)
-
11
case exp.first
-
when :multi
-
2
exp[1..-1].any? {|e| contains_nonempty_static?(e) }
-
when :escape
-
1
contains_nonempty_static?(exp.last)
-
when :static
-
9
!exp.last.empty?
-
else
-
false
-
end
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module HTML
-
# @api public
-
1
class Pretty < Fast
-
1
define_options indent: ' ',
-
pretty: true,
-
indent_tags: %w(article aside audio base body datalist dd div dl dt
-
fieldset figure footer form head h1 h2 h3 h4 h5 h6
-
header hgroup hr html li link meta nav ol option p
-
rp rt ruby section script style table tbody td tfoot
-
th thead tr ul video doctype).freeze,
-
pre_tags: %w(code pre textarea).freeze
-
-
1
def initialize(opts = {})
-
5
super
-
5
@indent_next = nil
-
5
@indent = 0
-
5
@pretty = options[:pretty]
-
20
@pre_tags = @format != :xml && Regexp.union(options[:pre_tags].map {|t| "<#{t}" })
-
end
-
-
1
def call(exp)
-
5
@pretty ? [:multi, preamble, compile(exp)] : super
-
end
-
-
1
def on_static(content)
-
108
return [:static, content] unless @pretty
-
unless @pre_tags && @pre_tags =~ content
-
content = content.sub(/\A\s*\n?/, "\n".freeze) if @indent_next
-
content = content.gsub("\n".freeze, indent)
-
end
-
@indent_next = false
-
[:static, content]
-
end
-
-
1
def on_dynamic(code)
-
2
return [:dynamic, code] unless @pretty
-
indent_next, @indent_next = @indent_next, false
-
[:dynamic, "::Temple::Utils.indent_dynamic((#{code}), #{indent_next.inspect}, #{indent.inspect}#{@pre_tags ? ', ' + @pre_tags_name : ''})"]
-
end
-
-
1
def on_html_doctype(type)
-
1
return super unless @pretty
-
[:multi, [:static, tag_indent('doctype')], super]
-
end
-
-
1
def on_html_comment(content)
-
return super unless @pretty
-
result = [:multi, [:static, tag_indent('comment')], super]
-
@indent_next = false
-
result
-
end
-
-
1
def on_html_tag(name, attrs, content = nil)
-
67
return super unless @pretty
-
-
name = name.to_s
-
closed = !content || (empty_exp?(content) && options[:autoclose].include?(name))
-
-
@pretty = false
-
result = [:multi, [:static, "#{tag_indent(name)}<#{name}"], compile(attrs)]
-
result << [:static, (closed && @format != :html ? ' /' : '') + '>']
-
-
@pretty = !@pre_tags || !options[:pre_tags].include?(name)
-
if content
-
@indent += 1
-
result << compile(content)
-
@indent -= 1
-
end
-
unless closed
-
indent = tag_indent(name)
-
result << [:static, "#{content && !empty_exp?(content) ? indent : ''}</#{name}>"]
-
end
-
@pretty = true
-
result
-
end
-
-
1
protected
-
-
1
def preamble
-
return [:multi] unless @pre_tags
-
@pre_tags_name = unique_name
-
[:code, "#{@pre_tags_name} = /#{@pre_tags.source}/"]
-
end
-
-
1
def indent
-
"\n" + (options[:indent] || '') * @indent
-
end
-
-
# Return indentation before tag
-
1
def tag_indent(name)
-
if @format == :xml
-
flag = @indent_next != nil
-
@indent_next = true
-
else
-
flag = @indent_next != nil && (@indent_next || options[:indent_tags].include?(name))
-
@indent_next = options[:indent_tags].include?(name)
-
end
-
flag ? indent : ''
-
end
-
end
-
end
-
end
-
1
module Temple
-
# Immutable map class which supports map merging
-
# @api public
-
1
class ImmutableMap
-
1
include Enumerable
-
-
1
def initialize(*map)
-
145
@map = map.compact
-
end
-
-
1
def include?(key)
-
18187
@map.any? {|h| h.include?(key) }
-
end
-
-
1
def [](key)
-
4055
@map.each {|h| return h[key] if h.include?(key) }
-
nil
-
end
-
-
1
def each
-
1415
keys.each {|k| yield(k, self[k]) }
-
end
-
-
1
def keys
-
1383
@map.inject([]) {|keys, h| keys.concat(h.keys) }.uniq
-
end
-
-
1
def values
-
keys.map {|k| self[k] }
-
end
-
-
1
def to_hash
-
205
result = {}
-
1415
each {|k, v| result[k] = v }
-
205
result
-
end
-
end
-
-
# Mutable map class which supports map merging
-
# @api public
-
1
class MutableMap < ImmutableMap
-
1
def initialize(*map)
-
40
super({}, *map)
-
end
-
-
1
def []=(key, value)
-
1
@map.first[key] = value
-
end
-
-
1
def update(map)
-
16
@map.first.update(map)
-
end
-
end
-
-
1
class OptionMap < MutableMap
-
1
def initialize(*map, &block)
-
40
super(*map)
-
40
@handler = block
-
40
@valid = {}
-
40
@deprecated = {}
-
end
-
-
1
def []=(key, value)
-
1
validate_key!(key)
-
1
super
-
end
-
-
1
def update(map)
-
16
validate_map!(map)
-
16
super
-
end
-
-
1
def valid_keys
-
(keys + @valid.keys +
-
150
@map.map {|h| h.valid_keys if h.respond_to?(:valid_keys) }.compact.flatten).uniq
-
end
-
-
1
def add_valid_keys(*keys)
-
238
keys.flatten.each { |key| @valid[key] = true }
-
end
-
-
1
def add_deprecated_keys(*keys)
-
keys.flatten.each { |key| @valid[key] = @deprecated[key] = true }
-
end
-
-
1
def validate_map!(map)
-
265
map.to_hash.keys.each {|key| validate_key!(key) }
-
end
-
-
1
def validate_key!(key)
-
145
@handler.call(self, key, :deprecated) if deprecated_key?(key)
-
145
@handler.call(self, key, :invalid) unless valid_key?(key)
-
end
-
-
1
def deprecated_key?(key)
-
@deprecated.include?(key) ||
-
1205
@map.any? {|h| h.deprecated_key?(key) if h.respond_to?(:deprecated_key?) }
-
end
-
-
1
def valid_key?(key)
-
3211
include?(key) || @valid.include?(key) ||
-
5000
@map.any? {|h| h.valid_key?(key) if h.respond_to?(:valid_key?) }
-
end
-
end
-
end
-
1
module Temple
-
1
module Mixins
-
# @api private
-
1
module CoreDispatcher
-
1
def on_multi(*exps)
-
2362
multi = [:multi]
-
6904
exps.each {|exp| multi << compile(exp) }
-
2362
multi
-
end
-
-
1
def on_capture(name, exp)
-
[:capture, name, compile(exp)]
-
end
-
end
-
-
# @api private
-
1
module EscapeDispatcher
-
1
def on_escape(flag, exp)
-
689
[:escape, flag, compile(exp)]
-
end
-
end
-
-
# @api private
-
1
module ControlFlowDispatcher
-
1
def on_if(condition, *cases)
-
[:if, condition, *cases.compact.map {|e| compile(e) }]
-
end
-
-
1
def on_case(arg, *cases)
-
[:case, arg, *cases.map {|condition, exp| [condition, compile(exp)] }]
-
end
-
-
1
def on_block(code, content)
-
[:block, code, compile(content)]
-
end
-
-
1
def on_cond(*cases)
-
[:cond, *cases.map {|condition, exp| [condition, compile(exp)] }]
-
end
-
end
-
-
# @api private
-
1
module CompiledDispatcher
-
1
def call(exp)
-
77
compile(exp)
-
end
-
-
1
def compile(exp)
-
10135
dispatcher(exp)
-
end
-
-
1
private
-
-
1
def dispatcher(exp)
-
17
replace_dispatcher(exp)
-
end
-
-
1
def replace_dispatcher(exp)
-
17
tree = DispatchNode.new
-
17
dispatched_methods.each do |method|
-
566
method.split('_'.freeze)[1..-1].inject(tree) {|node, type| node[type.to_sym] }.method = method
-
end
-
17
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def dispatcher(exp)
-
return replace_dispatcher(exp) if self.class != #{self.class}
-
#{tree.compile.gsub("\n", "\n ")}
-
end
-
RUBY
-
17
dispatcher(exp)
-
end
-
-
1
def dispatched_methods
-
17
re = /^on(_[a-zA-Z0-9]+)*$/
-
17
self.methods.map(&:to_s).select(&re.method(:=~))
-
end
-
-
# @api private
-
1
class DispatchNode < Hash
-
1
attr_accessor :method
-
-
1
def initialize
-
515
super { |hsh,key| hsh[key] = DispatchNode.new }
-
266
@method = nil
-
end
-
-
1
def compile(level = 0, call_parent = nil)
-
266
call_method = method ? (level == 0 ? "#{method}(*exp)" :
-
"#{method}(*exp[#{level}..-1])") : call_parent
-
266
if empty?
-
229
raise 'Invalid dispatcher node' unless method
-
229
call_method
-
else
-
37
code = "case(exp[#{level}])\n"
-
37
each do |key, child|
-
code << "when #{key.inspect}\n " <<
-
249
child.compile(level + 1, call_method).gsub("\n".freeze, "\n ".freeze) << "\n".freeze
-
end
-
37
code << "else\n " << (call_method || 'exp') << "\nend"
-
end
-
end
-
end
-
end
-
-
# @api public
-
#
-
# Implements a compatible call-method
-
# based on the including classe's methods.
-
#
-
# It uses every method starting with
-
# "on" and uses the rest of the method
-
# name as prefix of the expression it
-
# will receive. So, if a dispatcher
-
# has a method named "on_x", this method
-
# will be called with arg0,..,argN
-
# whenever an expression like [:x, arg0,..,argN ]
-
# is encountered.
-
#
-
# This works with longer prefixes, too.
-
# For example a method named "on_y_z"
-
# will be called whenever an expression
-
# like [:y, :z, .. ] is found. Furthermore,
-
# if additionally a method named "on_y"
-
# is present, it will be called when an
-
# expression starts with :y but then does
-
# not contain with :z. This way a
-
# dispatcher can implement namespaces.
-
#
-
# @note
-
# Processing does not reach into unknown
-
# expression types by default.
-
#
-
# @example
-
# class MyAwesomeDispatch
-
# include Temple::Mixins::Dispatcher
-
# def on_awesome(thing) # keep awesome things
-
# return [:awesome, thing]
-
# end
-
# def on_boring(thing) # make boring things awesome
-
# return [:awesome, thing+" with bacon"]
-
# end
-
# def on(type,*args) # unknown stuff is boring too
-
# return [:awesome, 'just bacon']
-
# end
-
# end
-
# filter = MyAwesomeDispatch.new
-
# # Boring things are converted:
-
# filter.call([:boring, 'egg']) #=> [:awesome, 'egg with bacon']
-
# # Unknown things too:
-
# filter.call([:foo]) #=> [:awesome, 'just bacon']
-
# # Known but not boring things won't be touched:
-
# filter.call([:awesome, 'chuck norris']) #=>[:awesome, 'chuck norris']
-
#
-
1
module Dispatcher
-
1
include CompiledDispatcher
-
1
include CoreDispatcher
-
1
include EscapeDispatcher
-
1
include ControlFlowDispatcher
-
end
-
end
-
end
-
1
module Temple
-
1
module Mixins
-
# @api private
-
1
module EngineDSL
-
1
def chain_modified!
-
end
-
-
1
def append(*args, &block)
-
19
chain << chain_element(args, block)
-
19
chain_modified!
-
end
-
-
1
def prepend(*args, &block)
-
chain.unshift(chain_element(args, block))
-
chain_modified!
-
end
-
-
1
def remove(name)
-
name = chain_name(name)
-
raise "#{name} not found" unless chain.reject! {|i| name === i.first }
-
chain_modified!
-
end
-
-
1
alias use append
-
-
1
def before(name, *args, &block)
-
name = chain_name(name)
-
e = chain_element(args, block)
-
chain.map! {|f| name === f.first ? [e, f] : [f] }.flatten!(1)
-
raise "#{name} not found" unless chain.include?(e)
-
chain_modified!
-
end
-
-
1
def after(name, *args, &block)
-
2
name = chain_name(name)
-
2
e = chain_element(args, block)
-
41
chain.map! {|f| name === f.first ? [f, e] : [f] }.flatten!(1)
-
2
raise "#{name} not found" unless chain.include?(e)
-
2
chain_modified!
-
end
-
-
1
def replace(name, *args, &block)
-
name = chain_name(name)
-
e = chain_element(args, block)
-
chain.map! {|f| name === f.first ? e : f }
-
raise "#{name} not found" unless chain.include?(e)
-
chain_modified!
-
end
-
-
# Shortcuts to access namespaces
-
{ filter: Temple::Filters,
-
generator: Temple::Generators,
-
1
html: Temple::HTML }.each do |method, mod|
-
3
define_method(method) do |name, *options|
-
9
use(name, mod.const_get(name), *options)
-
end
-
end
-
-
1
private
-
-
1
def chain_name(name)
-
2
case name
-
when Class
-
2
name.name.to_sym
-
when Symbol, String
-
name.to_sym
-
when Regexp
-
name
-
else
-
raise(ArgumentError, 'Name argument must be Class, Symbol, String or Regexp')
-
end
-
end
-
-
1
def chain_class_constructor(filter, local_options)
-
18
define_options(filter.options.valid_keys) if respond_to?(:define_options) && filter.respond_to?(:options)
-
18
proc do |engine|
-
90
opts = {}.update(engine.options)
-
1080
opts.delete_if {|k,v| !filter.options.valid_key?(k) } if filter.respond_to?(:options)
-
90
opts.update(local_options) if local_options
-
90
filter.new(opts)
-
end
-
end
-
-
1
def chain_proc_constructor(name, filter)
-
3
raise(ArgumentError, 'Proc or blocks must have arity 0 or 1') if filter.arity > 1
-
3
method_name = "FILTER #{name}"
-
3
c = Class === self ? self : singleton_class
-
6
filter = c.class_eval { define_method(method_name, &filter); instance_method(method_name) }
-
3
proc do |engine|
-
15
if filter.arity == 1
-
# the proc takes one argument, e.g. use(:Filter) {|exp| exp }
-
5
filter.bind(engine)
-
else
-
10
f = filter.bind(engine).call
-
10
if f.respond_to? :call
-
# the proc returns a callable object, e.g. use(:Filter) { Filter.new }
-
5
f
-
else
-
5
raise(ArgumentError, 'Proc or blocks must return a Callable or a Class') unless f.respond_to? :new
-
# the proc returns a class, e.g. use(:Filter) { Filter }
-
60
f.new(f.respond_to?(:options) ? engine.options.to_hash.select {|k,v| f.options.valid_key?(k) } : engine.options)
-
end
-
end
-
end
-
end
-
-
1
def chain_element(args, block)
-
21
name = args.shift
-
21
if Class === name
-
9
filter = name
-
9
name = filter.name.to_sym
-
else
-
12
raise(ArgumentError, 'Name argument must be Class or Symbol') unless Symbol === name
-
end
-
-
21
if block
-
3
raise(ArgumentError, 'Class and block argument are not allowed at the same time') if filter
-
3
filter = block
-
end
-
-
21
filter ||= args.shift
-
-
21
case filter
-
when Proc
-
# Proc or block argument
-
# The proc is converted to a method of the engine class.
-
# The proc can then access the option hash of the engine.
-
3
raise(ArgumentError, 'Too many arguments') unless args.empty?
-
3
[name, chain_proc_constructor(name, filter)]
-
when Class
-
# Class argument (e.g Filter class)
-
# The options are passed to the classes constructor.
-
18
raise(ArgumentError, 'Too many arguments') if args.size > 1
-
18
[name, chain_class_constructor(filter, args.first)]
-
else
-
# Other callable argument (e.g. Object of class which implements #call or Method)
-
# The callable has no access to the option hash of the engine.
-
raise(ArgumentError, 'Too many arguments') unless args.empty?
-
raise(ArgumentError, 'Class or callable argument is required') unless filter.respond_to?(:call)
-
[name, proc { filter }]
-
end
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Mixins
-
# @api public
-
1
module ClassOptions
-
1
def set_default_options(opts)
-
warn 'set_default_options has been deprecated, use set_options'
-
set_options(opts)
-
end
-
-
1
def default_options
-
warn 'default_options has been deprecated, use options'
-
options
-
end
-
-
1
def set_options(opts)
-
2
options.update(opts)
-
end
-
-
1
def options
-
40
@options ||= OptionMap.new(superclass.respond_to?(:options) ?
-
superclass.options : nil) do |hash, key, what|
-
3
warn "#{self}: Option #{key.inspect} is #{what}" unless @option_validator_disabled
-
1419
end
-
end
-
-
1
def define_options(*opts)
-
76
if opts.last.respond_to?(:to_hash)
-
13
hash = opts.pop.to_hash
-
13
options.add_valid_keys(hash.keys)
-
13
options.update(hash)
-
end
-
76
options.add_valid_keys(opts)
-
end
-
-
1
def define_deprecated_options(*opts)
-
if opts.last.respond_to?(:to_hash)
-
hash = opts.pop.to_hash
-
options.add_deprecated_keys(hash.keys)
-
options.update(hash)
-
end
-
options.add_deprecated_keys(opts)
-
end
-
-
1
def disable_option_validator!
-
3
@option_validator_disabled = true
-
end
-
end
-
-
1
module ThreadOptions
-
1
def with_options(options)
-
old_options = thread_options
-
Thread.current[thread_options_key] = ImmutableMap.new(options, thread_options)
-
yield
-
ensure
-
Thread.current[thread_options_key] = old_options
-
end
-
-
1
def thread_options
-
210
Thread.current[thread_options_key]
-
end
-
-
1
protected
-
-
1
def thread_options_key
-
210
@thread_options_key ||= "#{self.name}-thread-options".to_sym
-
end
-
end
-
-
# @api public
-
1
module Options
-
1
def self.included(base)
-
4
base.class_eval do
-
4
extend ClassOptions
-
4
extend ThreadOptions
-
end
-
end
-
-
1
attr_reader :options
-
-
1
def initialize(opts = {})
-
105
self.class.options.validate_map!(opts)
-
105
self.class.options.validate_map!(self.class.thread_options) if self.class.thread_options
-
105
@options = ImmutableMap.new({}.update(self.class.options).update(self.class.thread_options || {}).update(opts))
-
end
-
end
-
end
-
end
-
1
module Temple
-
1
module Mixins
-
# @api private
-
1
module Template
-
1
include ClassOptions
-
-
1
def compile(code, options)
-
5
engine = options.delete(:engine)
-
5
raise 'No engine configured' unless engine
-
5
engine.new(options).call(code)
-
end
-
-
1
def register_as(*names)
-
raise NotImplementedError
-
end
-
-
1
def create(engine, options)
-
1
register_as = options.delete(:register_as)
-
1
template = Class.new(self)
-
1
template.disable_option_validator!
-
1
template.options[:engine] = engine
-
1
template.options.update(options)
-
1
template.register_as(*register_as) if register_as
-
1
template
-
end
-
end
-
end
-
end
-
1
module Temple
-
# Temple base parser
-
# @api public
-
1
class Parser
-
1
include Utils
-
1
include Mixins::Options
-
end
-
end
-
1
module Temple
-
# @api public
-
1
module Templates
-
1
autoload :Tilt, 'temple/templates/tilt'
-
1
autoload :Rails, 'temple/templates/rails'
-
-
1
def self.method_missing(name, engine, options = {})
-
1
const_get(name).create(engine, options)
-
end
-
end
-
end
-
1
require 'tilt'
-
-
1
module Temple
-
1
module Templates
-
1
class Tilt < ::Tilt::Template
-
1
extend Mixins::Template
-
-
1
define_options mime_type: 'text/html'
-
-
1
def self.default_mime_type
-
options[:mime_type]
-
end
-
-
1
def self.default_mime_type=(mime_type)
-
options[:mime_type] = mime_type
-
end
-
-
# Prepare Temple template
-
#
-
# Called immediately after template data is loaded.
-
#
-
# @return [void]
-
1
def prepare
-
5
opts = {}.update(self.class.options).update(options).update(file: eval_file)
-
5
opts.delete(:mime_type)
-
5
if opts.include?(:outvar)
-
5
opts[:buffer] = opts.delete(:outvar)
-
5
opts[:save_buffer] = true
-
end
-
5
@src = self.class.compile(data, opts)
-
end
-
-
# A string containing the (Ruby) source code for the template.
-
#
-
# @param [Hash] locals Local variables
-
# @return [String] Compiled template ruby code
-
1
def precompiled_template(locals = {})
-
5
@src
-
end
-
-
1
def self.register_as(*names)
-
1
::Tilt.register(self, *names.map(&:to_s))
-
end
-
end
-
end
-
end
-
1
begin
-
1
require 'escape_utils'
-
rescue LoadError
-
1
begin
-
1
require 'cgi/escape'
-
rescue LoadError
-
end
-
end
-
-
1
module Temple
-
# @api public
-
1
module Utils
-
1
extend self
-
-
# Returns an escaped copy of `html`.
-
# Strings which are declared as html_safe are not escaped.
-
#
-
# @param html [String] The string to escape
-
# @return [String] The escaped string
-
1
def escape_html_safe(html)
-
html.html_safe? ? html : escape_html(html)
-
end
-
-
1
if defined?(EscapeUtils)
-
# Returns an escaped copy of `html`.
-
#
-
# @param html [String] The string to escape
-
# @return [String] The escaped string
-
def escape_html(html)
-
EscapeUtils.escape_html(html.to_s, false)
-
end
-
1
elsif defined?(CGI.escapeHTML)
-
# Returns an escaped copy of `html`.
-
#
-
# @param html [String] The string to escape
-
# @return [String] The escaped string
-
1
def escape_html(html)
-
79
CGI.escapeHTML(html.to_s)
-
end
-
else
-
# Used by escape_html
-
# @api private
-
ESCAPE_HTML = {
-
'&' => '&',
-
'"' => '"',
-
'\'' => ''',
-
'<' => '<',
-
'>' => '>'
-
}.freeze
-
-
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
-
-
# Returns an escaped copy of `html`.
-
#
-
# @param html [String] The string to escape
-
# @return [String] The escaped string
-
def escape_html(html)
-
html.to_s.gsub(ESCAPE_HTML_PATTERN, ESCAPE_HTML)
-
end
-
end
-
-
# Generate unique variable name
-
#
-
# @param prefix [String] Variable name prefix
-
# @return [String] Variable name
-
1
def unique_name(prefix = nil)
-
5
@unique_name ||= 0
-
5
prefix ||= (@unique_prefix ||= self.class.name.gsub('::'.freeze, '_'.freeze).downcase)
-
5
"_#{prefix}#{@unique_name += 1}"
-
end
-
-
# Check if expression is empty
-
#
-
# @param exp [Array] Temple expression
-
# @return true if expression is empty
-
1
def empty_exp?(exp)
-
186
case exp[0]
-
when :multi
-
197
exp[1..-1].all? {|e| empty_exp?(e) }
-
when :newline
-
54
true
-
else
-
43
false
-
end
-
end
-
-
1
def indent_dynamic(text, indent_next, indent, pre_tags = nil)
-
text = text.to_s
-
safe = text.respond_to?(:html_safe?) && text.html_safe?
-
return text if pre_tags && text =~ pre_tags
-
-
level = text.scan(/^\s*/).map(&:size).min
-
text = text.gsub(/(?!\A)^\s{#{level}}/, '') if level > 0
-
-
text = text.sub(/\A\s*\n?/, "\n".freeze) if indent_next
-
text = text.gsub("\n".freeze, indent)
-
-
safe ? text.html_safe : text
-
end
-
end
-
end
-
1
module Temple
-
1
VERSION = '0.7.7'
-
end
-
1
require 'tilt/mapping'
-
1
require 'tilt/template'
-
-
# Namespace for Tilt. This module is not intended to be included anywhere.
-
1
module Tilt
-
# Current version.
-
1
VERSION = '2.0.6'
-
-
1
@default_mapping = Mapping.new
-
-
# @return [Tilt::Mapping] the main mapping object
-
1
def self.default_mapping
-
48
@default_mapping
-
end
-
-
# @private
-
1
def self.lazy_map
-
default_mapping.lazy_map
-
end
-
-
# @see Tilt::Mapping#register
-
1
def self.register(template_class, *extensions)
-
1
default_mapping.register(template_class, *extensions)
-
end
-
-
# @see Tilt::Mapping#register_lazy
-
1
def self.register_lazy(class_name, file, *extensions)
-
42
default_mapping.register_lazy(class_name, file, *extensions)
-
end
-
-
# @deprecated Use {register} instead.
-
1
def self.prefer(template_class, *extensions)
-
register(template_class, *extensions)
-
end
-
-
# @see Tilt::Mapping#registered?
-
1
def self.registered?(ext)
-
default_mapping.registered?(ext)
-
end
-
-
# @see Tilt::Mapping#new
-
1
def self.new(file, line=nil, options={}, &block)
-
default_mapping.new(file, line, options, &block)
-
end
-
-
# @see Tilt::Mapping#[]
-
1
def self.[](file)
-
5
default_mapping[file]
-
end
-
-
# @see Tilt::Mapping#template_for
-
1
def self.template_for(file)
-
default_mapping.template_for(file)
-
end
-
-
# @see Tilt::Mapping#templates_for
-
1
def self.templates_for(file)
-
default_mapping.templates_for(file)
-
end
-
-
# @return the template object that is currently rendering.
-
#
-
# @example
-
# tmpl = Tilt['index.erb'].new { '<%= Tilt.current_template %>' }
-
# tmpl.render == tmpl.to_s
-
#
-
# @note This is currently an experimental feature and might return nil
-
# in the future.
-
1
def self.current_template
-
Thread.current[:tilt_current_template]
-
end
-
-
# Extremely simple template cache implementation. Calling applications
-
# create a Tilt::Cache instance and use #fetch with any set of hashable
-
# arguments (such as those to Tilt.new):
-
#
-
# cache = Tilt::Cache.new
-
# cache.fetch(path, line, options) { Tilt.new(path, line, options) }
-
#
-
# Subsequent invocations return the already loaded template object.
-
#
-
# @note
-
# Tilt::Cache is a thin wrapper around Hash. It has the following
-
# limitations:
-
# * Not thread-safe.
-
# * Size is unbounded.
-
# * Keys are not copied defensively, and should not be modified after
-
# being passed to #fetch. More specifically, the values returned by
-
# key#hash and key#eql? should not change.
-
# If this is too limiting for you, use a different cache implementation.
-
1
class Cache
-
1
def initialize
-
1
@cache = {}
-
end
-
-
# Caches a value for key, or returns the previously cached value.
-
# If a value has been previously cached for key then it is
-
# returned. Otherwise, block is yielded to and its return value
-
# which may be nil, is cached under key and returned.
-
# @yield
-
# @yieldreturn the value to cache for key
-
1
def fetch(*key)
-
56
@cache.fetch(key) do
-
5
@cache[key] = yield
-
end
-
end
-
-
# Clears the cache.
-
1
def clear
-
@cache = {}
-
end
-
end
-
-
-
# Template Implementations ================================================
-
-
# ERB
-
1
register_lazy :ERBTemplate, 'tilt/erb', 'erb', 'rhtml'
-
1
register_lazy :ErubisTemplate, 'tilt/erubis', 'erb', 'rhtml', 'erubis'
-
1
register_lazy :ErubiTemplate, 'tilt/erubi', 'erb', 'rhtml', 'erubi'
-
-
# Markdown
-
1
register_lazy :BlueClothTemplate, 'tilt/bluecloth', 'markdown', 'mkd', 'md'
-
1
register_lazy :MarukuTemplate, 'tilt/maruku', 'markdown', 'mkd', 'md'
-
1
register_lazy :KramdownTemplate, 'tilt/kramdown', 'markdown', 'mkd', 'md'
-
1
register_lazy :RDiscountTemplate, 'tilt/rdiscount', 'markdown', 'mkd', 'md'
-
1
register_lazy :RedcarpetTemplate, 'tilt/redcarpet', 'markdown', 'mkd', 'md'
-
1
register_lazy :CommonMarkerTemplate, 'tilt/commonmarker', 'markdown', 'mkd', 'md'
-
1
register_lazy :PandocTemplate, 'tilt/pandoc', 'markdown', 'mkd', 'md'
-
-
# Rest (sorted by name)
-
1
register_lazy :AsciidoctorTemplate, 'tilt/asciidoc', 'ad', 'adoc', 'asciidoc'
-
1
register_lazy :BabelTemplate, 'tilt/babel', 'es6', 'babel', 'jsx'
-
1
register_lazy :BuilderTemplate, 'tilt/builder', 'builder'
-
1
register_lazy :CSVTemplate, 'tilt/csv', 'rcsv'
-
1
register_lazy :CoffeeScriptTemplate, 'tilt/coffee', 'coffee'
-
1
register_lazy :CoffeeScriptLiterateTemplate, 'tilt/coffee', 'litcoffee'
-
1
register_lazy :CreoleTemplate, 'tilt/creole', 'wiki', 'creole'
-
1
register_lazy :EtanniTemplate, 'tilt/etanni', 'etn', 'etanni'
-
1
register_lazy :HamlTemplate, 'tilt/haml', 'haml'
-
1
register_lazy :LessTemplate, 'tilt/less', 'less'
-
1
register_lazy :LiquidTemplate, 'tilt/liquid', 'liquid'
-
1
register_lazy :LiveScriptTemplate, 'tilt/livescript','ls'
-
1
register_lazy :MarkabyTemplate, 'tilt/markaby', 'mab'
-
1
register_lazy :NokogiriTemplate, 'tilt/nokogiri', 'nokogiri'
-
1
register_lazy :PlainTemplate, 'tilt/plain', 'html'
-
1
register_lazy :PrawnTemplate, 'tilt/prawn', 'prawn'
-
1
register_lazy :RDocTemplate, 'tilt/rdoc', 'rdoc'
-
1
register_lazy :RadiusTemplate, 'tilt/radius', 'radius'
-
1
register_lazy :RedClothTemplate, 'tilt/redcloth', 'textile'
-
1
register_lazy :RstPandocTemplate, 'tilt/rst-pandoc', 'rst'
-
1
register_lazy :SassTemplate, 'tilt/sass', 'sass'
-
1
register_lazy :ScssTemplate, 'tilt/sass', 'scss'
-
1
register_lazy :SigilTemplate, 'tilt/sigil', 'sigil'
-
1
register_lazy :StringTemplate, 'tilt/string', 'str'
-
1
register_lazy :TypeScriptTemplate, 'tilt/typescript', 'ts'
-
1
register_lazy :WikiClothTemplate, 'tilt/wikicloth', 'wiki', 'mediawiki', 'mw'
-
1
register_lazy :YajlTemplate, 'tilt/yajl', 'yajl'
-
-
# External template engines
-
1
register_lazy 'Slim::Template', 'slim', 'slim'
-
1
register_lazy 'Tilt::HandlebarsTemplate', 'tilt/handlebars', 'handlebars', 'hbs'
-
1
register_lazy 'Tilt::OrgTemplate', 'org-ruby', 'org'
-
1
register_lazy 'Opal::Processor', 'opal', 'opal', 'rb'
-
1
register_lazy 'Tilt::JbuilderTemplate', 'tilt/jbuilder', 'jbuilder'
-
end
-
# Used for detecting autoloading bug in JRuby
-
1
class Tilt::Dummy; end
-
-
1
require 'monitor'
-
-
1
module Tilt
-
# Tilt::Mapping associates file extensions with template implementations.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register(Tilt::RDocTemplate, 'rdoc')
-
# mapping['index.rdoc'] # => Tilt::RDocTemplate
-
# mapping.new('index.rdoc').render
-
#
-
# You can use {#register} to register a template class by file
-
# extension, {#registered?} to see if a file extension is mapped,
-
# {#[]} to lookup template classes, and {#new} to instantiate template
-
# objects.
-
#
-
# Mapping also supports *lazy* template implementations. Note that regularly
-
# registered template implementations *always* have preference over lazily
-
# registered template implementations. You should use {#register} if you
-
# depend on a specific template implementation and {#register_lazy} if there
-
# are multiple alternatives.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
-
# mapping['index.md']
-
# # => RDiscount::Template
-
#
-
# {#register_lazy} takes a class name, a filename, and a list of file
-
# extensions. When you try to lookup a template name that matches the
-
# file extension, Tilt will automatically try to require the filename and
-
# constantize the class name.
-
#
-
# Unlike {#register}, there can be multiple template implementations
-
# registered lazily to the same file extension. Tilt will attempt to load the
-
# template implementations in order (registered *last* would be tried first),
-
# returning the first which doesn't raise LoadError.
-
#
-
# If all of the registered template implementations fails, Tilt will raise
-
# the exception of the first, since that was the most preferred one.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register_lazy('Bluecloth::Template', 'bluecloth/template', 'md')
-
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
-
# mapping['index.md']
-
# # => RDiscount::Template
-
#
-
# In the previous example we say that RDiscount has a *higher priority* than
-
# BlueCloth. Tilt will first try to `require "rdiscount/template"`, falling
-
# back to `require "bluecloth/template"`. If none of these are successful,
-
# the first error will be raised.
-
1
class Mapping
-
# @private
-
1
attr_reader :lazy_map, :template_map
-
-
1
def initialize
-
1
@template_map = Hash.new
-
49
@lazy_map = Hash.new { |h, k| h[k] = [] }
-
end
-
-
# @private
-
1
def initialize_copy(other)
-
@template_map = other.template_map.dup
-
@lazy_map = other.lazy_map.dup
-
end
-
-
# Registers a lazy template implementation by file extension. You
-
# can have multiple lazy template implementations defined on the
-
# same file extension, in which case the template implementation
-
# defined *last* will be attempted loaded *first*.
-
#
-
# @param class_name [String] Class name of a template class.
-
# @param file [String] Filename where the template class is defined.
-
# @param extensions [Array<String>] List of extensions.
-
# @return [void]
-
#
-
# @example
-
# mapping.register_lazy 'MyEngine::Template', 'my_engine/template', 'mt'
-
#
-
# defined?(MyEngine::Template) # => false
-
# mapping['index.mt'] # => MyEngine::Template
-
# defined?(MyEngine::Template) # => true
-
1
def register_lazy(class_name, file, *extensions)
-
# Internal API
-
42
if class_name.is_a?(Symbol)
-
37
Tilt.autoload class_name, file
-
37
class_name = "Tilt::#{class_name}"
-
end
-
-
42
extensions.each do |ext|
-
71
@lazy_map[ext].unshift([class_name, file])
-
end
-
end
-
-
# Registers a template implementation by file extension. There can only be
-
# one template implementation per file extension, and this method will
-
# override any existing mapping.
-
#
-
# @param template_class
-
# @param extensions [Array<String>] List of extensions.
-
# @return [void]
-
#
-
# @example
-
# mapping.register MyEngine::Template, 'mt'
-
# mapping['index.mt'] # => MyEngine::Template
-
1
def register(template_class, *extensions)
-
1
if template_class.respond_to?(:to_str)
-
# Support register(ext, template_class) too
-
extensions, template_class = [template_class], extensions[0]
-
end
-
-
1
extensions.each do |ext|
-
1
@template_map[ext.to_s] = template_class
-
end
-
end
-
-
# Checks if a file extension is registered (either eagerly or
-
# lazily) in this mapping.
-
#
-
# @param ext [String] File extension.
-
#
-
# @example
-
# mapping.registered?('erb') # => true
-
# mapping.registered?('nope') # => false
-
1
def registered?(ext)
-
5
@template_map.has_key?(ext.downcase) or lazy?(ext)
-
end
-
-
# Instantiates a new template class based on the file.
-
#
-
# @raise [RuntimeError] if there is no template class registered for the
-
# file name.
-
#
-
# @example
-
# mapping.new('index.mt') # => instance of MyEngine::Template
-
#
-
# @see Tilt::Template.new
-
1
def new(file, line=nil, options={}, &block)
-
if template_class = self[file]
-
template_class.new(file, line, options, &block)
-
else
-
fail "No template engine registered for #{File.basename(file)}"
-
end
-
end
-
-
# Looks up a template class based on file name and/or extension.
-
#
-
# @example
-
# mapping['views/hello.erb'] # => Tilt::ERBTemplate
-
# mapping['hello.erb'] # => Tilt::ERBTemplate
-
# mapping['erb'] # => Tilt::ERBTemplate
-
#
-
# @return [template class]
-
1
def [](file)
-
5
_, ext = split(file)
-
5
ext && lookup(ext)
-
end
-
-
1
alias template_for []
-
-
# Looks up a list of template classes based on file name. If the file name
-
# has multiple extensions, it will return all template classes matching the
-
# extensions from the end.
-
#
-
# @example
-
# mapping.templates_for('views/index.haml.erb')
-
# # => [Tilt::ERBTemplate, Tilt::HamlTemplate]
-
#
-
# @return [Array<template class>]
-
1
def templates_for(file)
-
templates = []
-
-
while true
-
prefix, ext = split(file)
-
break unless ext
-
templates << lookup(ext)
-
file = prefix
-
end
-
-
templates
-
end
-
-
# Finds the extensions the template class has been registered under.
-
# @param [template class] template_class
-
1
def extensions_for(template_class)
-
res = []
-
template_map.each do |ext, klass|
-
res << ext if template_class == klass
-
end
-
lazy_map.each do |ext, choices|
-
res << ext if choices.any? { |klass, file| template_class.to_s == klass }
-
end
-
res
-
end
-
-
1
private
-
-
1
def lazy?(ext)
-
ext = ext.downcase
-
@lazy_map.has_key?(ext) && !@lazy_map[ext].empty?
-
end
-
-
1
def split(file)
-
5
pattern = file.to_s.downcase
-
5
full_pattern = pattern.dup
-
-
5
until registered?(pattern)
-
return if pattern.empty?
-
pattern = File.basename(pattern)
-
pattern.sub!(/^[^.]*\.?/, '')
-
end
-
-
5
prefix_size = full_pattern.size - pattern.size
-
5
[full_pattern[0,prefix_size-1], pattern]
-
end
-
-
1
def lookup(ext)
-
5
@template_map[ext] || lazy_load(ext)
-
end
-
-
1
LOCK = Monitor.new
-
-
1
def lazy_load(pattern)
-
return unless @lazy_map.has_key?(pattern)
-
-
LOCK.enter
-
entered = true
-
-
choices = @lazy_map[pattern]
-
-
# Check if a template class is already present
-
choices.each do |class_name, file|
-
template_class = constant_defined?(class_name)
-
if template_class
-
register(template_class, pattern)
-
return template_class
-
end
-
end
-
-
first_failure = nil
-
-
# Load in order
-
choices.each do |class_name, file|
-
begin
-
require file
-
# It's safe to eval() here because constant_defined? will
-
# raise NameError on invalid constant names
-
template_class = eval(class_name)
-
rescue LoadError => ex
-
first_failure ||= ex
-
else
-
register(template_class, pattern)
-
return template_class
-
end
-
end
-
-
raise first_failure if first_failure
-
ensure
-
LOCK.exit if entered
-
end
-
-
# This is due to a bug in JRuby (see GH issue jruby/jruby#3585)
-
1
Tilt.autoload :Dummy, "tilt/dummy"
-
1
require "tilt/dummy"
-
1
AUTOLOAD_IS_BROKEN = Tilt.autoload?(:Dummy)
-
-
# The proper behavior (in MRI) for autoload? is to
-
# return `false` when the constant/file has been
-
# explicitly required.
-
#
-
# However, in JRuby it returns `true` even after it's
-
# been required. In that case it turns out that `defined?`
-
# returns `"constant"` if it exists and `nil` when it doesn't.
-
# This is actually a second bug: `defined?` should resolve
-
# autoload (aka. actually try to require the file).
-
#
-
# We use the second bug in order to resolve the first bug.
-
-
1
def constant_defined?(name)
-
name.split('::').inject(Object) do |scope, n|
-
if scope.autoload?(n)
-
if !AUTOLOAD_IS_BROKEN
-
return false
-
end
-
-
if eval("!defined?(scope::#{n})")
-
return false
-
end
-
end
-
return false if !scope.const_defined?(n)
-
scope.const_get(n)
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
-
1
module Tilt
-
# @private
-
1
TOPOBJECT = Object.superclass || Object
-
# @private
-
1
LOCK = Mutex.new
-
-
# Base class for template implementations. Subclasses must implement
-
# the #prepare method and one of the #evaluate or #precompiled_template
-
# methods.
-
1
class Template
-
# Template source; loaded from a file or given directly.
-
1
attr_reader :data
-
-
# The name of the file where the template data was loaded from.
-
1
attr_reader :file
-
-
# The line number in #file where template data was loaded from.
-
1
attr_reader :line
-
-
# A Hash of template engine specific options. This is passed directly
-
# to the underlying engine and is not used by the generic template
-
# interface.
-
1
attr_reader :options
-
-
1
class << self
-
# An empty Hash that the template engine can populate with various
-
# metadata.
-
1
def metadata
-
@metadata ||= {}
-
end
-
-
# @deprecated Use `.metadata[:mime_type]` instead.
-
1
def default_mime_type
-
metadata[:mime_type]
-
end
-
-
# @deprecated Use `.metadata[:mime_type] = val` instead.
-
1
def default_mime_type=(value)
-
metadata[:mime_type] = value
-
end
-
end
-
-
# Create a new template with the file, line, and options specified. By
-
# default, template data is read from the file. When a block is given,
-
# it should read template data and return as a String. When file is nil,
-
# a block is required.
-
#
-
# All arguments are optional.
-
1
def initialize(file=nil, line=1, options={}, &block)
-
5
@file, @line, @options = nil, 1, {}
-
-
5
[options, line, file].compact.each do |arg|
-
case
-
5
when arg.respond_to?(:to_str) ; @file = arg.to_str
-
5
when arg.respond_to?(:to_int) ; @line = arg.to_int
-
5
when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
-
when arg.respond_to?(:path) ; @file = arg.path
-
when arg.respond_to?(:to_path) ; @file = arg.to_path
-
else raise TypeError, "Can't load the template file. Pass a string with a path " +
-
"or an object that responds to 'to_str', 'path' or 'to_path'"
-
15
end
-
end
-
-
5
raise ArgumentError, "file or block required" if (@file || block).nil?
-
-
# used to hold compiled template methods
-
5
@compiled_method = {}
-
-
# used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
-
# currently only used if template compiles to ruby
-
5
@default_encoding = @options.delete :default_encoding
-
-
# load template data and prepare (uses binread to avoid encoding issues)
-
10
@reader = block || lambda { |t| read_template_file }
-
5
@data = @reader.call(self)
-
-
5
if @data.respond_to?(:force_encoding)
-
5
if default_encoding
-
5
@data = @data.dup if @data.frozen?
-
5
@data.force_encoding(default_encoding)
-
end
-
-
5
if !@data.valid_encoding?
-
raise Encoding::InvalidByteSequenceError, "#{eval_file} is not valid #{@data.encoding}"
-
end
-
end
-
-
5
prepare
-
end
-
-
# Render the template in the given scope with the locals specified. If a
-
# block is given, it is typically available within the template via
-
# +yield+.
-
1
def render(scope=nil, locals={}, &block)
-
56
scope ||= Object.new
-
56
current_template = Thread.current[:tilt_current_template]
-
56
Thread.current[:tilt_current_template] = self
-
56
evaluate(scope, locals || {}, &block)
-
ensure
-
56
Thread.current[:tilt_current_template] = current_template
-
end
-
-
# The basename of the template file.
-
1
def basename(suffix='')
-
File.basename(file, suffix) if file
-
end
-
-
# The template file's basename with all extensions chomped off.
-
1
def name
-
basename.split('.', 2).first if basename
-
end
-
-
# The filename used in backtraces to describe the template.
-
1
def eval_file
-
10
file || '(__TEMPLATE__)'
-
end
-
-
# An empty Hash that the template engine can populate with various
-
# metadata.
-
1
def metadata
-
if respond_to?(:allows_script?)
-
self.class.metadata.merge(:allows_script => allows_script?)
-
else
-
self.class.metadata
-
end
-
end
-
-
1
protected
-
-
# @!group For template implementations
-
-
# The encoding of the source data. Defaults to the
-
# default_encoding-option if present. You may override this method
-
# in your template class if you have a better hint of the data's
-
# encoding.
-
1
def default_encoding
-
10
@default_encoding
-
end
-
-
# Do whatever preparation is necessary to setup the underlying template
-
# engine. Called immediately after template data is loaded. Instance
-
# variables set in this method are available when #evaluate is called.
-
#
-
# Subclasses must provide an implementation of this method.
-
1
def prepare
-
raise NotImplementedError
-
end
-
-
# Execute the compiled template and return the result string. Template
-
# evaluation is guaranteed to be performed in the scope object with the
-
# locals specified and with support for yielding to the block.
-
#
-
# This method is only used by source generating templates. Subclasses that
-
# override render() may not support all features.
-
1
def evaluate(scope, locals, &block)
-
56
locals_keys = locals.keys
-
56
locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
-
56
method = compiled_method(locals_keys)
-
56
method.bind(scope).call(locals, &block)
-
end
-
-
# Generates all template source by combining the preamble, template, and
-
# postamble and returns a two-tuple of the form: [source, offset], where
-
# source is the string containing (Ruby) source code for the template and
-
# offset is the integer line offset where line reporting should begin.
-
#
-
# Template subclasses may override this method when they need complete
-
# control over source generation or want to adjust the default line
-
# offset. In most cases, overriding the #precompiled_template method is
-
# easier and more appropriate.
-
1
def precompiled(local_keys)
-
5
preamble = precompiled_preamble(local_keys)
-
5
template = precompiled_template(local_keys)
-
5
postamble = precompiled_postamble(local_keys)
-
5
source = String.new
-
-
# Ensure that our generated source code has the same encoding as the
-
# the source code generated by the template engine.
-
5
if source.respond_to?(:force_encoding)
-
5
template_encoding = extract_encoding(template)
-
-
5
source.force_encoding(template_encoding)
-
5
template.force_encoding(template_encoding)
-
end
-
-
5
source << preamble << "\n" << template << "\n" << postamble
-
-
5
[source, preamble.count("\n")+1]
-
end
-
-
# A string containing the (Ruby) source code for the template. The
-
# default Template#evaluate implementation requires either this
-
# method or the #precompiled method be overridden. When defined,
-
# the base Template guarantees correct file/line handling, locals
-
# support, custom scopes, proper encoding, and support for template
-
# compilation.
-
1
def precompiled_template(local_keys)
-
raise NotImplementedError
-
end
-
-
1
def precompiled_preamble(local_keys)
-
5
''
-
end
-
-
1
def precompiled_postamble(local_keys)
-
5
''
-
end
-
-
# !@endgroup
-
-
1
private
-
-
1
def read_template_file
-
10
data = File.open(file, 'rb') { |io| io.read }
-
5
if data.respond_to?(:force_encoding)
-
# Set it to the default external (without verifying)
-
5
data.force_encoding(Encoding.default_external) if Encoding.default_external
-
end
-
5
data
-
end
-
-
# The compiled method for the locals keys provided.
-
1
def compiled_method(locals_keys)
-
56
LOCK.synchronize do
-
56
@compiled_method[locals_keys] ||= compile_template_method(locals_keys)
-
end
-
end
-
-
1
def local_extraction(local_keys)
-
local_keys.map do |k|
-
if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
-
"#{k} = locals[#{k.inspect}]"
-
else
-
raise "invalid locals key: #{k.inspect} (keys must be variable names)"
-
end
-
5
end.join("\n")
-
end
-
-
1
def compile_template_method(local_keys)
-
5
source, offset = precompiled(local_keys)
-
5
local_code = local_extraction(local_keys)
-
-
5
method_name = "__tilt_#{Thread.current.object_id.abs}"
-
5
method_source = String.new
-
-
5
if method_source.respond_to?(:force_encoding)
-
5
method_source.force_encoding(source.encoding)
-
end
-
-
method_source << <<-RUBY
-
TOPOBJECT.class_eval do
-
def #{method_name}(locals)
-
Thread.current[:tilt_vars] = [self, locals]
-
class << self
-
this, locals = Thread.current[:tilt_vars]
-
this.instance_eval do
-
#{local_code}
-
5
RUBY
-
5
offset += method_source.count("\n")
-
5
method_source << source
-
5
method_source << "\nend;end;end;end"
-
5
Object.class_eval(method_source, eval_file, line - offset)
-
5
unbind_compiled_method(method_name)
-
end
-
-
1
def unbind_compiled_method(method_name)
-
5
method = TOPOBJECT.instance_method(method_name)
-
10
TOPOBJECT.class_eval { remove_method(method_name) }
-
5
method
-
end
-
-
1
def extract_encoding(script)
-
5
extract_magic_comment(script) || script.encoding
-
end
-
-
1
def extract_magic_comment(script)
-
5
binary(script) do
-
5
script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
-
end
-
end
-
-
1
def binary(string)
-
5
original_encoding = string.encoding
-
5
string.force_encoding(Encoding::BINARY)
-
5
yield
-
ensure
-
5
string.force_encoding(original_encoding)
-
end
-
end
-
end
-
# Protocol references:
-
#
-
# * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
-
# * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
-
# * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
-
-
1
require 'base64'
-
1
require 'digest/md5'
-
1
require 'digest/sha1'
-
1
require 'securerandom'
-
1
require 'set'
-
1
require 'stringio'
-
1
require 'uri'
-
1
require 'websocket/extensions'
-
-
1
module WebSocket
-
1
autoload :HTTP, File.expand_path('../http', __FILE__)
-
-
1
class Driver
-
-
1
root = File.expand_path('../driver', __FILE__)
-
-
1
begin
-
# Load C native extension
-
1
require 'websocket_mask'
-
rescue LoadError
-
# Fall back to pure-Ruby implementation
-
require 'websocket/mask'
-
end
-
-
-
1
if RUBY_PLATFORM =~ /java/
-
require 'jruby'
-
com.jcoglan.websocket.WebsocketMaskService.new.basicLoad(JRuby.runtime)
-
end
-
-
1
unless Mask.respond_to?(:mask)
-
def Mask.mask(payload, mask)
-
@instance ||= new
-
@instance.mask(payload, mask)
-
end
-
end
-
-
1
MAX_LENGTH = 0x3ffffff
-
1
STATES = [:connecting, :open, :closing, :closed]
-
-
1
BINARY = 'ASCII-8BIT'
-
1
UNICODE = 'UTF-8'
-
-
1
ConnectEvent = Struct.new(nil)
-
1
OpenEvent = Struct.new(nil)
-
1
MessageEvent = Struct.new(:data)
-
1
CloseEvent = Struct.new(:code, :reason)
-
-
1
ProtocolError = Class.new(StandardError)
-
1
URIError = Class.new(ArgumentError)
-
1
ConfigurationError = Class.new(ArgumentError)
-
-
1
autoload :Client, root + '/client'
-
1
autoload :Draft75, root + '/draft75'
-
1
autoload :Draft76, root + '/draft76'
-
1
autoload :EventEmitter, root + '/event_emitter'
-
1
autoload :Headers, root + '/headers'
-
1
autoload :Hybi, root + '/hybi'
-
1
autoload :Proxy, root + '/proxy'
-
1
autoload :Server, root + '/server'
-
1
autoload :StreamReader, root + '/stream_reader'
-
-
1
include EventEmitter
-
1
attr_reader :protocol, :ready_state
-
-
1
def initialize(socket, options = {})
-
2
super()
-
2
Driver.validate_options(options, [:max_length, :masking, :require_masking, :protocols])
-
-
2
@socket = socket
-
2
@reader = StreamReader.new
-
2
@options = options
-
2
@max_length = options[:max_length] || MAX_LENGTH
-
2
@headers = Headers.new
-
2
@queue = []
-
2
@ready_state = 0
-
end
-
-
1
def state
-
return nil unless @ready_state >= 0
-
STATES[@ready_state]
-
end
-
-
1
def add_extension(extension)
-
false
-
end
-
-
1
def set_header(name, value)
-
return false unless @ready_state <= 0
-
@headers[name] = value
-
true
-
end
-
-
1
def start
-
1
return false unless @ready_state == 0
-
1
response = handshake_response
-
1
return false unless response
-
1
@socket.write(response)
-
1
open unless @stage == -1
-
1
true
-
end
-
-
1
def text(message)
-
456
message = message.encode(UNICODE) unless message.encoding.name == UNICODE
-
456
frame(message, :text)
-
end
-
-
1
def binary(message)
-
false
-
end
-
-
1
def ping(*args)
-
false
-
end
-
-
1
def pong(*args)
-
false
-
end
-
-
1
def close(reason = nil, code = nil)
-
return false unless @ready_state == 1
-
@ready_state = 3
-
emit(:close, CloseEvent.new(nil, nil))
-
true
-
end
-
-
1
private
-
-
1
def open
-
1
@ready_state = 1
-
2
@queue.each { |message| frame(*message) }
-
1
@queue = []
-
1
emit(:open, OpenEvent.new)
-
end
-
-
1
def queue(message)
-
1
@queue << message
-
1
true
-
end
-
-
1
def self.client(socket, options = {})
-
Client.new(socket, options.merge(:masking => true))
-
end
-
-
1
def self.server(socket, options = {})
-
1
Server.new(socket, options.merge(:require_masking => true))
-
end
-
-
1
def self.rack(socket, options = {})
-
1
env = socket.env
-
1
if env['HTTP_SEC_WEBSOCKET_VERSION']
-
1
Hybi.new(socket, options.merge(:require_masking => true))
-
elsif env['HTTP_SEC_WEBSOCKET_KEY1']
-
Draft76.new(socket, options)
-
else
-
Draft75.new(socket, options)
-
end
-
end
-
-
1
def self.encode(string, encoding = nil)
-
456
case string
-
when Array then
-
string = string.pack('C*')
-
encoding ||= BINARY
-
when String then
-
456
encoding ||= UNICODE
-
end
-
456
unless string.encoding.name == encoding
-
456
string = string.dup if string.frozen?
-
456
string.force_encoding(encoding)
-
end
-
456
string.valid_encoding? ? string : nil
-
end
-
-
1
def self.validate_options(options, valid_keys)
-
2
options.keys.each do |key|
-
2
unless valid_keys.include?(key)
-
raise ConfigurationError, "Unrecognized option: #{key.inspect}"
-
end
-
end
-
end
-
-
1
def self.websocket?(env)
-
connection = env['HTTP_CONNECTION'] || ''
-
upgrade = env['HTTP_UPGRADE'] || ''
-
-
env['REQUEST_METHOD'] == 'GET' and
-
connection.downcase.split(/ *, */).include?('upgrade') and
-
upgrade.downcase == 'websocket'
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
-
1
module EventEmitter
-
1
def initialize
-
9
@listeners = Hash.new { |h,k| h[k] = [] }
-
end
-
-
1
def add_listener(event, callable = nil, &block)
-
6
listener = callable || block
-
6
@listeners[event.to_s] << listener
-
6
listener
-
end
-
-
1
def on(event, callable = nil, &block)
-
6
if callable
-
add_listener(event, callable)
-
else
-
6
add_listener(event, &block)
-
end
-
end
-
-
1
def remove_listener(event, callable = nil, &block)
-
listener = callable || block
-
@listeners[event.to_s].delete(listener)
-
listener
-
end
-
-
1
def remove_all_listeners(event = nil)
-
if event
-
@listeners.delete(event.to_s)
-
else
-
@listeners.clear
-
end
-
end
-
-
1
def emit(event, *args)
-
915
@listeners[event.to_s].dup.each do |listener|
-
914
listener.call(*args)
-
end
-
end
-
-
1
def listener_count(event)
-
return 0 unless @listeners.has_key?(event.to_s)
-
@listeners[event.to_s].size
-
end
-
-
1
def listeners(event)
-
@listeners[event.to_s]
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
-
1
class Headers
-
1
ALLOWED_DUPLICATES = %w[set-cookie set-cookie2 warning www-authenticate]
-
-
1
def initialize(received = {})
-
2
@raw = received
-
2
clear
-
-
2
@received = {}
-
2
@raw.each { |k,v| @received[HTTP.normalize_header(k)] = v }
-
end
-
-
1
def clear
-
2
@sent = Set.new
-
2
@lines = []
-
end
-
-
1
def [](name)
-
@received[HTTP.normalize_header(name)]
-
end
-
-
1
def []=(name, value)
-
3
return if value.nil?
-
3
key = HTTP.normalize_header(name)
-
3
return unless @sent.add?(key) or ALLOWED_DUPLICATES.include?(key)
-
3
@lines << "#{name.strip}: #{value.to_s.strip}\r\n"
-
end
-
-
1
def inspect
-
@raw.inspect
-
end
-
-
1
def to_h
-
@raw.dup
-
end
-
-
1
def to_s
-
1
@lines.join('')
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
-
1
class Hybi < Driver
-
1
root = File.expand_path('../hybi', __FILE__)
-
-
1
autoload :Frame, root + '/frame'
-
1
autoload :Message, root + '/message'
-
-
1
def self.generate_accept(key)
-
1
Base64.strict_encode64(Digest::SHA1.digest(key + GUID))
-
end
-
-
1
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
-
-
1
BYTE = 0b11111111
-
1
FIN = MASK = 0b10000000
-
1
RSV1 = 0b01000000
-
1
RSV2 = 0b00100000
-
1
RSV3 = 0b00010000
-
1
OPCODE = 0b00001111
-
1
LENGTH = 0b01111111
-
-
1
OPCODES = {
-
:continuation => 0,
-
:text => 1,
-
:binary => 2,
-
:close => 8,
-
:ping => 9,
-
:pong => 10
-
}
-
-
1
OPCODE_CODES = OPCODES.values
-
1
MESSAGE_OPCODES = OPCODES.values_at(:continuation, :text, :binary)
-
1
OPENING_OPCODES = OPCODES.values_at(:text, :binary)
-
-
1
ERRORS = {
-
:normal_closure => 1000,
-
:going_away => 1001,
-
:protocol_error => 1002,
-
:unacceptable => 1003,
-
:encoding_error => 1007,
-
:policy_violation => 1008,
-
:too_large => 1009,
-
:extension_error => 1010,
-
:unexpected_condition => 1011
-
}
-
-
1
ERROR_CODES = ERRORS.values
-
1
DEFAULT_ERROR_CODE = 1000
-
1
MIN_RESERVED_ERROR = 3000
-
1
MAX_RESERVED_ERROR = 4999
-
-
1
PACK_FORMATS = {2 => 'n', 8 => 'Q>'}
-
-
1
def initialize(socket, options = {})
-
1
super
-
-
1
@extensions = ::WebSocket::Extensions.new
-
1
@stage = 0
-
1
@masking = options[:masking]
-
1
@protocols = options[:protocols] || []
-
1
@protocols = @protocols.strip.split(/ *, */) if String === @protocols
-
1
@require_masking = options[:require_masking]
-
1
@ping_callbacks = {}
-
-
1
@frame = @message = nil
-
-
1
return unless @socket.respond_to?(:env)
-
-
1
sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
-
1
protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
-
-
1
@headers['Upgrade'] = 'websocket'
-
1
@headers['Connection'] = 'Upgrade'
-
1
@headers['Sec-WebSocket-Accept'] = Hybi.generate_accept(sec_key)
-
-
1
if protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
-
protos = protos.split(/ *, */) if String === protos
-
@protocol = protos.find { |p| @protocols.include?(p) }
-
@headers['Sec-WebSocket-Protocol'] = @protocol if @protocol
-
end
-
end
-
-
1
def version
-
"hybi-#{@socket.env['HTTP_SEC_WEBSOCKET_VERSION']}"
-
end
-
-
1
def add_extension(extension)
-
@extensions.add(extension)
-
true
-
end
-
-
1
def parse(chunk)
-
456
@reader.put(chunk)
-
456
buffer = true
-
456
while buffer
-
2281
case @stage
-
when 0 then
-
912
buffer = @reader.read(1)
-
912
parse_opcode(buffer.getbyte(0)) if buffer
-
-
when 1 then
-
456
buffer = @reader.read(1)
-
456
parse_length(buffer.getbyte(0)) if buffer
-
-
when 2 then
-
1
buffer = @reader.read(@frame.length_bytes)
-
1
parse_extended_length(buffer) if buffer
-
-
when 3 then
-
456
buffer = @reader.read(4)
-
456
if buffer
-
456
@stage = 4
-
456
@frame.masking_key = buffer
-
end
-
-
when 4 then
-
456
buffer = @reader.read(@frame.length)
-
-
456
if buffer
-
456
@stage = 0
-
456
emit_frame(buffer)
-
end
-
-
else
-
buffer = nil
-
end
-
end
-
end
-
-
1
def binary(message)
-
frame(message, :binary)
-
end
-
-
1
def ping(message = '', &callback)
-
@ping_callbacks[message] = callback if callback
-
frame(message, :ping)
-
end
-
-
1
def pong(message = '')
-
frame(message, :pong)
-
end
-
-
1
def close(reason = nil, code = nil)
-
reason ||= ''
-
code ||= ERRORS[:normal_closure]
-
-
if @ready_state <= 0
-
@ready_state = 3
-
emit(:close, CloseEvent.new(code, reason))
-
true
-
elsif @ready_state == 1
-
frame(reason, :close, code)
-
@ready_state = 2
-
true
-
else
-
false
-
end
-
end
-
-
1
def frame(buffer, type = nil, code = nil)
-
457
return queue([buffer, type, code]) if @ready_state <= 0
-
456
return false unless @ready_state == 1
-
-
456
message = Message.new
-
456
frame = Frame.new
-
456
is_text = String === buffer
-
-
456
message.rsv1 = message.rsv2 = message.rsv3 = false
-
456
message.opcode = OPCODES[type || (is_text ? :text : :binary)]
-
-
456
payload = is_text ? buffer.bytes.to_a : buffer
-
456
payload = [code].pack(PACK_FORMATS[2]).bytes.to_a + payload if code
-
456
message.data = payload.pack('C*')
-
-
456
if MESSAGE_OPCODES.include?(message.opcode)
-
456
message = @extensions.process_outgoing_message(message)
-
end
-
-
456
frame.final = true
-
456
frame.rsv1 = message.rsv1
-
456
frame.rsv2 = message.rsv2
-
456
frame.rsv3 = message.rsv3
-
456
frame.opcode = message.opcode
-
456
frame.masked = !!@masking
-
456
frame.masking_key = SecureRandom.random_bytes(4) if frame.masked
-
456
frame.length = message.data.bytesize
-
456
frame.payload = message.data
-
-
456
send_frame(frame)
-
456
true
-
-
rescue ::WebSocket::Extensions::ExtensionError => error
-
fail(:extension_error, error.message)
-
end
-
-
1
private
-
-
1
def send_frame(frame)
-
456
length = frame.length
-
456
buffer = []
-
456
masked = frame.masked ? MASK : 0
-
-
456
buffer[0] = (frame.final ? FIN : 0) |
-
456
(frame.rsv1 ? RSV1 : 0) |
-
456
(frame.rsv2 ? RSV2 : 0) |
-
456
(frame.rsv3 ? RSV3 : 0) |
-
frame.opcode
-
-
456
if length <= 125
-
379
buffer[1] = masked | length
-
77
elsif length <= 65535
-
77
buffer[1] = masked | 126
-
77
buffer[2..3] = [length].pack(PACK_FORMATS[2]).bytes.to_a
-
else
-
buffer[1] = masked | 127
-
buffer[2..9] = [length].pack(PACK_FORMATS[8]).bytes.to_a
-
end
-
-
456
if frame.masked
-
buffer.concat(frame.masking_key.bytes.to_a)
-
buffer.concat(Mask.mask(frame.payload, frame.masking_key).bytes.to_a)
-
else
-
456
buffer.concat(frame.payload.bytes.to_a)
-
end
-
-
456
@socket.write(buffer.pack('C*'))
-
end
-
-
1
def handshake_response
-
1
begin
-
1
extensions = @extensions.generate_response(@socket.env['HTTP_SEC_WEBSOCKET_EXTENSIONS'])
-
rescue => error
-
fail(:protocol_error, error.message)
-
return nil
-
end
-
-
1
@headers['Sec-WebSocket-Extensions'] = extensions if extensions
-
-
1
start = 'HTTP/1.1 101 Switching Protocols'
-
1
headers = [start, @headers.to_s, '']
-
1
headers.join("\r\n")
-
end
-
-
1
def shutdown(code, reason, error = false)
-
@frame = @message = nil
-
@stage = 5
-
@extensions.close
-
-
frame(reason, :close, code) if @ready_state < 2
-
@ready_state = 3
-
-
emit(:error, ProtocolError.new(reason)) if error
-
emit(:close, CloseEvent.new(code, reason))
-
end
-
-
1
def fail(type, message)
-
return if @ready_state > 1
-
shutdown(ERRORS[type], message, true)
-
end
-
-
1
def parse_opcode(octet)
-
1824
rsvs = [RSV1, RSV2, RSV3].map { |rsv| (octet & rsv) == rsv }
-
-
456
@frame = Frame.new
-
-
456
@frame.final = (octet & FIN) == FIN
-
456
@frame.rsv1 = rsvs[0]
-
456
@frame.rsv2 = rsvs[1]
-
456
@frame.rsv3 = rsvs[2]
-
456
@frame.opcode = (octet & OPCODE)
-
-
456
@stage = 1
-
-
456
unless @extensions.valid_frame_rsv?(@frame)
-
return fail(:protocol_error,
-
"One or more reserved bits are on: reserved1 = #{@frame.rsv1 ? 1 : 0}" +
-
", reserved2 = #{@frame.rsv2 ? 1 : 0 }" +
-
", reserved3 = #{@frame.rsv3 ? 1 : 0 }")
-
end
-
-
456
unless OPCODES.values.include?(@frame.opcode)
-
return fail(:protocol_error, "Unrecognized frame opcode: #{@frame.opcode}")
-
end
-
-
456
unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.final
-
return fail(:protocol_error, "Received fragmented control frame: opcode = #{@frame.opcode}")
-
end
-
-
456
if @message and OPENING_OPCODES.include?(@frame.opcode)
-
return fail(:protocol_error, 'Received new data frame but previous continuous frame is unfinished')
-
end
-
end
-
-
1
def parse_length(octet)
-
456
@frame.masked = (octet & MASK) == MASK
-
456
@frame.length = (octet & LENGTH)
-
-
456
if @frame.length >= 0 and @frame.length <= 125
-
455
@stage = @frame.masked ? 3 : 4
-
455
return unless check_frame_length
-
else
-
1
@stage = 2
-
1
@frame.length_bytes = (@frame.length == 126) ? 2 : 8
-
end
-
-
456
if @require_masking and not @frame.masked
-
return fail(:unacceptable, 'Received unmasked frame but masking is required')
-
end
-
end
-
-
1
def parse_extended_length(buffer)
-
1
@frame.length = buffer.unpack(PACK_FORMATS[buffer.bytesize]).first
-
1
@stage = @frame.masked ? 3 : 4
-
-
1
unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.length <= 125
-
return fail(:protocol_error, "Received control frame having too long payload: #{@frame.length}")
-
end
-
-
1
return unless check_frame_length
-
end
-
-
1
def check_frame_length
-
456
length = @message ? @message.data.bytesize : 0
-
-
456
if length + @frame.length > @max_length
-
fail(:too_large, 'WebSocket frame length too large')
-
false
-
else
-
456
true
-
end
-
end
-
-
1
def emit_frame(buffer)
-
456
frame = @frame
-
456
opcode = frame.opcode
-
456
payload = frame.payload = Mask.mask(buffer, @frame.masking_key)
-
456
bytesize = payload.bytesize
-
456
bytes = payload.bytes.to_a
-
-
456
@frame = nil
-
-
456
case opcode
-
when OPCODES[:continuation] then
-
return fail(:protocol_error, 'Received unexpected continuation frame') unless @message
-
@message << frame
-
-
when OPCODES[:text], OPCODES[:binary] then
-
456
@message = Message.new
-
456
@message << frame
-
-
when OPCODES[:close] then
-
code = (bytesize >= 2) ? payload.unpack(PACK_FORMATS[2]).first : nil
-
reason = (bytesize > 2) ? Driver.encode(bytes[2..-1] || [], UNICODE) : nil
-
-
unless (bytesize == 0) or
-
(code && code >= MIN_RESERVED_ERROR && code <= MAX_RESERVED_ERROR) or
-
ERROR_CODES.include?(code)
-
code = ERRORS[:protocol_error]
-
end
-
-
if bytesize > 125 or (bytesize > 2 and reason.nil?)
-
code = ERRORS[:protocol_error]
-
end
-
-
shutdown(code || DEFAULT_ERROR_CODE, reason || '')
-
-
when OPCODES[:ping] then
-
frame(payload, :pong)
-
-
when OPCODES[:pong] then
-
message = Driver.encode(payload, UNICODE)
-
callback = @ping_callbacks[message]
-
@ping_callbacks.delete(message)
-
callback.call if callback
-
end
-
-
456
emit_message if frame.final and MESSAGE_OPCODES.include?(opcode)
-
end
-
-
1
def emit_message
-
456
message = @extensions.process_incoming_message(@message)
-
456
@message = nil
-
-
456
payload = message.data
-
-
456
case message.opcode
-
when OPCODES[:text] then
-
456
payload = Driver.encode(payload, UNICODE)
-
when OPCODES[:binary]
-
payload = payload.bytes.to_a
-
end
-
-
456
if payload
-
456
emit(:message, MessageEvent.new(payload))
-
else
-
fail(:encoding_error, 'Could not decode a text frame as UTF-8')
-
end
-
rescue ::WebSocket::Extensions::ExtensionError => error
-
fail(:extension_error, error.message)
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
1
class Hybi
-
-
1
class Frame
-
1
attr_accessor :final,
-
:rsv1,
-
:rsv2,
-
:rsv3,
-
:opcode,
-
:masked,
-
:masking_key,
-
:length_bytes,
-
:length,
-
:payload
-
end
-
-
end
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
1
class Hybi
-
-
1
class Message
-
1
attr_accessor :rsv1,
-
:rsv2,
-
:rsv3,
-
:opcode,
-
:data
-
-
1
def initialize
-
912
@rsv1 = false
-
912
@rsv2 = false
-
912
@rsv3 = false
-
912
@opcode = nil
-
912
@data = String.new('').force_encoding(BINARY)
-
end
-
-
1
def <<(frame)
-
456
@rsv1 ||= frame.rsv1
-
456
@rsv2 ||= frame.rsv2
-
456
@rsv3 ||= frame.rsv3
-
456
@opcode ||= frame.opcode
-
456
@data << frame.payload
-
end
-
end
-
-
end
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
-
1
class Server < Driver
-
1
EVENTS = %w[open message error close]
-
-
1
def initialize(socket, options = {})
-
1
super
-
1
@http = HTTP::Request.new
-
1
@delegate = nil
-
end
-
-
1
def env
-
5
@http.complete? ? @http.env : nil
-
end
-
-
1
def url
-
return nil unless e = env
-
-
url = "ws://#{e['HTTP_HOST']}"
-
url << e['PATH_INFO']
-
url << "?#{e['QUERY_STRING']}" unless e['QUERY_STRING'] == ''
-
url
-
end
-
-
1
%w[add_extension set_header start frame text binary ping close].each do |method|
-
8
define_method(method) do |*args, &block|
-
457
if @delegate
-
456
@delegate.__send__(method, *args, &block)
-
else
-
1
@queue << [method, args, block]
-
1
true
-
end
-
end
-
end
-
-
1
%w[protocol version].each do |method|
-
2
define_method(method) do
-
@delegate && @delegate.__send__(method)
-
end
-
end
-
-
1
def parse(chunk)
-
457
return @delegate.parse(chunk) if @delegate
-
-
1
@http.parse(chunk)
-
1
return fail_request('Invalid HTTP request') if @http.error?
-
1
return unless @http.complete?
-
-
1
@delegate = Driver.rack(self, @options)
-
1
open
-
-
1
EVENTS.each do |event|
-
461
@delegate.on(event) { |e| emit(event, e) }
-
end
-
-
1
emit(:connect, ConnectEvent.new)
-
end
-
-
1
def write(buffer)
-
457
@socket.write(buffer)
-
end
-
-
1
private
-
-
1
def fail_request(message)
-
emit(:error, ProtocolError.new(message))
-
emit(:close, CloseEvent.new(Hybi::ERRORS[:protocol_error], message))
-
end
-
-
1
def open
-
1
@queue.each do |method, args, block|
-
1
@delegate.__send__(method, *args, &block)
-
end
-
1
@queue = []
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Driver
-
-
1
class StreamReader
-
# Try to minimise the number of reallocations done:
-
1
MINIMUM_AUTOMATIC_PRUNE_OFFSET = 128
-
-
1
def initialize
-
2
@buffer = String.new('').force_encoding(BINARY)
-
2
@offset = 0
-
end
-
-
1
def put(chunk)
-
456
return unless chunk and chunk.bytesize > 0
-
456
@buffer << chunk.force_encoding(BINARY)
-
end
-
-
# Read bytes from the data:
-
1
def read(length)
-
2281
return nil if (@offset + length) > @buffer.bytesize
-
-
1825
chunk = @buffer.byteslice(@offset, length)
-
1825
@offset += chunk.bytesize
-
-
1825
prune if @offset > MINIMUM_AUTOMATIC_PRUNE_OFFSET
-
-
1825
return chunk
-
end
-
-
1
def each_byte
-
prune
-
-
@buffer.each_byte do |octet|
-
@offset += 1
-
yield octet
-
end
-
end
-
-
1
private
-
-
1
def prune
-
228
buffer_size = @buffer.bytesize
-
-
228
if @offset > buffer_size
-
@buffer = String.new('').force_encoding(BINARY)
-
else
-
228
@buffer = @buffer.byteslice(@offset, buffer_size - @offset)
-
end
-
-
228
@offset = 0
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
module HTTP
-
-
1
root = File.expand_path('../http', __FILE__)
-
-
1
autoload :Headers, root + '/headers'
-
1
autoload :Request, root + '/request'
-
1
autoload :Response, root + '/response'
-
-
1
def self.normalize_header(name)
-
13
name.to_s.strip.downcase.gsub(/^http_/, '').gsub(/_/, '-')
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
module HTTP
-
-
1
module Headers
-
1
MAX_LINE_LENGTH = 4096
-
1
CR = 0x0D
-
1
LF = 0x0A
-
-
# RFC 2616 grammar rules:
-
#
-
# CHAR = <any US-ASCII character (octets 0 - 127)>
-
#
-
# CTL = <any US-ASCII control character
-
# (octets 0 - 31) and DEL (127)>
-
#
-
# SP = <US-ASCII SP, space (32)>
-
#
-
# HT = <US-ASCII HT, horizontal-tab (9)>
-
#
-
# token = 1*<any CHAR except CTLs or separators>
-
#
-
# separators = "(" | ")" | "<" | ">" | "@"
-
# | "," | ";" | ":" | "\" | <">
-
# | "/" | "[" | "]" | "?" | "="
-
# | "{" | "}" | SP | HT
-
#
-
# Or, as redefined in RFC 7230:
-
#
-
# token = 1*tchar
-
#
-
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
-
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
-
# / DIGIT / ALPHA
-
# ; any VCHAR, except delimiters
-
-
1
HEADER_LINE = /^([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+):\s*([\x20-\x7e]*?)\s*$/i
-
-
1
attr_reader :headers
-
-
1
def initialize
-
1
@buffer = []
-
1
@env = {}
-
1
@headers = {}
-
1
@stage = 0
-
end
-
-
1
def complete?
-
6
@stage == 2
-
end
-
-
1
def error?
-
1
@stage == -1
-
end
-
-
1
def parse(chunk)
-
1
chunk.each_byte do |octet|
-
384
if octet == LF and @stage < 2
-
12
@buffer.pop if @buffer.last == CR
-
12
if @buffer.empty?
-
1
complete if @stage == 1
-
else
-
11
result = case @stage
-
1
when 0 then start_line(string_buffer)
-
10
when 1 then header_line(string_buffer)
-
end
-
-
11
if result
-
11
@stage = 1
-
else
-
error
-
end
-
end
-
12
@buffer = []
-
else
-
372
@buffer << octet if @stage >= 0
-
372
error if @stage < 2 and @buffer.size > MAX_LINE_LENGTH
-
end
-
end
-
1
@env['rack.input'] = StringIO.new(string_buffer)
-
end
-
-
1
private
-
-
1
def complete
-
1
@stage = 2
-
end
-
-
1
def error
-
@stage = -1
-
end
-
-
1
def header_line(line)
-
10
return false unless parsed = line.scan(HEADER_LINE).first
-
-
10
key = HTTP.normalize_header(parsed[0])
-
10
value = parsed[1].strip
-
-
10
if @headers.has_key?(key)
-
@headers[key] << ', ' << value
-
else
-
10
@headers[key] = value
-
end
-
10
true
-
end
-
-
1
def string_buffer
-
12
@buffer.pack('C*')
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
module HTTP
-
-
1
class Request
-
1
include Headers
-
-
1
REQUEST_LINE = /^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) ([\x21-\x7e]+) (HTTP\/[0-9]+\.[0-9]+)$/
-
1
REQUEST_TARGET = /^(.*?)(\?(.*))?$/
-
1
RESERVED_HEADERS = %w[content-length content-type]
-
-
1
attr_reader :env
-
-
1
private
-
-
1
def start_line(line)
-
1
return false unless parsed = line.scan(REQUEST_LINE).first
-
-
1
target = parsed[1].scan(REQUEST_TARGET).first
-
-
1
@env = {
-
'REQUEST_METHOD' => parsed[0],
-
'SCRIPT_NAME' => '',
-
'PATH_INFO' => target[0],
-
'QUERY_STRING' => target[2] || ''
-
}
-
1
true
-
end
-
-
1
def complete
-
1
super
-
1
@headers.each do |name, value|
-
10
rack_name = name.upcase.gsub(/-/, '_')
-
10
rack_name = "HTTP_#{rack_name}" unless RESERVED_HEADERS.include?(name)
-
10
@env[rack_name] = value
-
end
-
1
if host = @env['HTTP_HOST']
-
1
uri = URI.parse("http://#{host}")
-
1
@env['SERVER_NAME'] = uri.host
-
1
@env['SERVER_PORT'] = uri.port.to_s
-
end
-
end
-
end
-
-
end
-
end
-
1
module WebSocket
-
1
class Extensions
-
-
1
autoload :Parser, File.expand_path('../extensions/parser', __FILE__)
-
-
1
ExtensionError = Class.new(ArgumentError)
-
-
1
MESSAGE_OPCODES = [1, 2]
-
-
1
def initialize
-
1
@rsv1 = @rsv2 = @rsv3 = nil
-
-
1
@by_name = {}
-
1
@in_order = []
-
1
@sessions = []
-
1
@index = {}
-
end
-
-
1
def add(ext)
-
unless ext.respond_to?(:name) and ext.name.is_a?(String)
-
raise TypeError, 'extension.name must be a string'
-
end
-
-
unless ext.respond_to?(:type) and ext.type == 'permessage'
-
raise TypeError, 'extension.type must be "permessage"'
-
end
-
-
unless ext.respond_to?(:rsv1) and [true, false].include?(ext.rsv1)
-
raise TypeError, 'extension.rsv1 must be true or false'
-
end
-
-
unless ext.respond_to?(:rsv2) and [true, false].include?(ext.rsv2)
-
raise TypeError, 'extension.rsv2 must be true or false'
-
end
-
-
unless ext.respond_to?(:rsv3) and [true, false].include?(ext.rsv3)
-
raise TypeError, 'extension.rsv3 must be true or false'
-
end
-
-
if @by_name.has_key?(ext.name)
-
raise TypeError, %Q{An extension with name "#{ext.name}" is already registered}
-
end
-
-
@by_name[ext.name] = ext
-
@in_order.push(ext)
-
end
-
-
1
def generate_offer
-
sessions = []
-
offer = []
-
index = {}
-
-
@in_order.each do |ext|
-
session = ext.create_client_session
-
next unless session
-
-
record = [ext, session]
-
sessions.push(record)
-
index[ext.name] = record
-
-
offers = session.generate_offer
-
offers = offers ? [offers].flatten : []
-
-
offers.each do |off|
-
offer.push(Parser.serialize_params(ext.name, off))
-
end
-
end
-
-
@sessions = sessions
-
@index = index
-
-
offer.size > 0 ? offer.join(', ') : nil
-
end
-
-
1
def activate(header)
-
responses = Parser.parse_header(header)
-
@sessions = []
-
-
responses.each_offer do |name, params|
-
unless record = @index[name]
-
raise ExtensionError, %Q{Server sent am extension response for unknown extension "#{name}"}
-
end
-
-
ext, session = *record
-
-
if reserved = reserved?(ext)
-
raise ExtensionError, %Q{Server sent two extension responses that use the RSV#{reserved[0]} } +
-
%Q{ bit: "#{reserved[1]}" and "#{ext.name}"}
-
end
-
-
unless session.activate(params) == true
-
raise ExtensionError, %Q{Server send unacceptable extension parameters: #{Parser.serialize_params(name, params)}}
-
end
-
-
reserve(ext)
-
@sessions.push(record)
-
end
-
end
-
-
1
def generate_response(header)
-
1
offers = Parser.parse_header(header)
-
1
sessions = []
-
1
response = []
-
-
1
@in_order.each do |ext|
-
offer = offers.by_name(ext.name)
-
next if offer.empty? or reserved?(ext)
-
-
next unless session = ext.create_server_session(offer)
-
-
reserve(ext)
-
sessions.push([ext, session])
-
response.push(Parser.serialize_params(ext.name, session.generate_response))
-
end
-
-
1
@sessions = sessions
-
1
response.size > 0 ? response.join(', ') : nil
-
end
-
-
1
def valid_frame_rsv(frame)
-
456
allowed = {:rsv1 => false, :rsv2 => false, :rsv3 => false}
-
-
456
if MESSAGE_OPCODES.include?(frame.opcode)
-
456
@sessions.each do |ext, session|
-
allowed[:rsv1] ||= ext.rsv1
-
allowed[:rsv2] ||= ext.rsv2
-
allowed[:rsv3] ||= ext.rsv3
-
end
-
end
-
-
456
(allowed[:rsv1] || !frame.rsv1) &&
-
912
(allowed[:rsv2] || !frame.rsv2) &&
-
456
(allowed[:rsv3] || !frame.rsv3)
-
end
-
1
alias :valid_frame_rsv? :valid_frame_rsv
-
-
1
def process_incoming_message(message)
-
456
@sessions.reverse.inject(message) do |msg, (ext, session)|
-
begin
-
session.process_incoming_message(msg)
-
rescue => error
-
raise ExtensionError, [ext.name, error.message].join(': ')
-
end
-
end
-
end
-
-
1
def process_outgoing_message(message)
-
456
@sessions.inject(message) do |msg, (ext, session)|
-
begin
-
session.process_outgoing_message(msg)
-
rescue => error
-
raise ExtensionError, [ext.name, error.message].join(': ')
-
end
-
end
-
end
-
-
1
def close
-
return unless @sessions
-
-
@sessions.each do |ext, session|
-
session.close rescue nil
-
end
-
end
-
-
1
private
-
-
1
def reserve(ext)
-
@rsv1 ||= ext.rsv1 && ext.name
-
@rsv2 ||= ext.rsv2 && ext.name
-
@rsv3 ||= ext.rsv3 && ext.name
-
end
-
-
1
def reserved?(ext)
-
return [1, @rsv1] if @rsv1 and ext.rsv1
-
return [2, @rsv2] if @rsv2 and ext.rsv2
-
return [3, @rsv3] if @rsv3 and ext.rsv3
-
false
-
end
-
-
end
-
end
-
1
require 'strscan'
-
-
1
module WebSocket
-
1
class Extensions
-
-
1
class Parser
-
1
TOKEN = /([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+)/
-
1
NOTOKEN = /([^!#\$%&'\*\+\-\.\^_`\|~0-9a-z])/
-
1
QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"/
-
1
PARAM = %r{#{TOKEN.source}(?:=(?:#{TOKEN.source}|#{QUOTED.source}))?}
-
1
EXT = %r{#{TOKEN.source}(?: *; *#{PARAM.source})*}
-
1
EXT_LIST = %r{^#{EXT.source}(?: *, *#{EXT.source})*$}
-
1
NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/
-
-
1
ParseError = Class.new(ArgumentError)
-
-
1
def self.parse_header(header)
-
1
offers = Offers.new
-
1
return offers if header == '' or header.nil?
-
-
1
unless header =~ EXT_LIST
-
raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{header}"
-
end
-
-
1
scanner = StringScanner.new(header)
-
1
value = scanner.scan(EXT)
-
-
1
until value.nil?
-
1
params = value.scan(PARAM)
-
1
name = params.shift.first
-
1
offer = {}
-
-
1
params.each do |key, unquoted, quoted|
-
if unquoted
-
data = unquoted
-
elsif quoted
-
data = quoted.gsub(/\\/, '')
-
else
-
data = true
-
end
-
if data =~ NUMBER
-
data = data =~ /\./ ? data.to_f : data.to_i(10)
-
end
-
-
if offer.has_key?(key)
-
offer[key] = [*offer[key]] + [data]
-
else
-
offer[key] = data
-
end
-
end
-
-
1
offers.push(name, offer)
-
-
1
scanner.scan(/ *, */)
-
1
value = scanner.scan(EXT)
-
end
-
1
offers
-
end
-
-
1
def self.serialize_params(name, params)
-
values = []
-
-
print = lambda do |key, value|
-
case value
-
when Array then value.each { |v| print[key, v] }
-
when true then values.push(key)
-
when Numeric then values.push(key + '=' + value.to_s)
-
else
-
if value =~ NOTOKEN
-
values.push(key + '="' + value.gsub(/"/, '\"') + '"')
-
else
-
values.push(key + '=' + value)
-
end
-
end
-
end
-
-
params.keys.sort.each { |key| print[key, params[key]] }
-
-
([name] + values).join('; ')
-
end
-
end
-
-
1
class Offers
-
1
def initialize
-
1
@by_name = {}
-
1
@in_order = []
-
end
-
-
1
def push(name, params)
-
1
@by_name[name] ||= []
-
1
@by_name[name].push(params)
-
1
@in_order.push(:name => name, :params => params)
-
end
-
-
1
def each_offer(&block)
-
@in_order.each do |offer|
-
block.call(offer[:name], offer[:params])
-
end
-
end
-
-
1
def by_name(name)
-
@by_name[name] || []
-
end
-
-
1
def to_a
-
@in_order.dup
-
end
-
end
-
-
end
-
end
-
1
require 'nokogiri'
-
-
1
require 'xpath/dsl'
-
1
require 'xpath/expression'
-
1
require 'xpath/literal'
-
1
require 'xpath/union'
-
1
require 'xpath/renderer'
-
1
require 'xpath/html'
-
-
1
module XPath
-
-
1
extend XPath::DSL::TopLevel
-
1
include XPath::DSL::TopLevel
-
-
1
def self.generate
-
yield(self)
-
end
-
end
-
1
module XPath
-
1
module DSL
-
1
module TopLevel
-
1
def current
-
741
Expression.new(:this_node)
-
end
-
-
1
def name
-
Expression.new(:node_name, current)
-
end
-
-
1
def descendant(*expressions)
-
247
Expression.new(:descendant, current, expressions)
-
end
-
-
1
def child(*expressions)
-
Expression.new(:child, current, expressions)
-
end
-
-
1
def axis(name, tag_name=:*)
-
Expression.new(:axis, current, name, tag_name)
-
end
-
-
1
def next_sibling(*expressions)
-
Expression.new(:next_sibling, current, expressions)
-
end
-
-
1
def previous_sibling(*expressions)
-
Expression.new(:previous_sibling, current, expressions)
-
end
-
-
1
def anywhere(*expressions)
-
61
Expression.new(:anywhere, expressions)
-
end
-
-
1
def attr(expression)
-
478
Expression.new(:attribute, current, expression)
-
end
-
-
1
def contains(expression)
-
Expression.new(:contains, current, expression)
-
end
-
-
1
def starts_with(expression)
-
Expression.new(:starts_with, current, expression)
-
end
-
-
1
def text
-
Expression.new(:text, current)
-
end
-
-
1
def string
-
138
Expression.new(:string_function, current)
-
end
-
-
1
def css(selector)
-
Expression.new(:css, current, Literal.new(selector))
-
end
-
end
-
-
1
module ExpressionLevel
-
1
include XPath::DSL::TopLevel
-
-
1
def where(expression)
-
712
Expression.new(:where, current, expression)
-
end
-
1
alias_method :[], :where
-
-
1
def one_of(*expressions)
-
77
Expression.new(:one_of, current, expressions)
-
end
-
-
1
def equals(expression)
-
276
Expression.new(:equality, current, expression)
-
end
-
1
alias_method :==, :equals
-
-
1
def is(expression)
-
202
Expression.new(:is, current, expression)
-
end
-
-
1
def or(expression)
-
247
Expression.new(:or, current, expression)
-
end
-
1
alias_method :|, :or
-
-
1
def and(expression)
-
Expression.new(:and, current, expression)
-
end
-
1
alias_method :&, :and
-
-
1
def union(*expressions)
-
109
Union.new(*[self, expressions].flatten)
-
end
-
1
alias_method :+, :union
-
-
1
def inverse
-
61
Expression.new(:inverse, current)
-
end
-
1
alias_method :~, :inverse
-
-
1
def string_literal
-
Expression.new(:string_literal, self)
-
end
-
-
1
def normalize
-
138
Expression.new(:normalized_space, current)
-
end
-
1
alias_method :n, :normalize
-
end
-
end
-
end
-
1
module XPath
-
1
class Expression
-
1
attr_accessor :expression, :arguments
-
1
include XPath::DSL::ExpressionLevel
-
-
1
def initialize(expression, *arguments)
-
3378
@expression = expression
-
3378
@arguments = arguments
-
end
-
-
1
def current
-
1835
self
-
end
-
-
1
def to_xpath(type=nil)
-
Renderer.render(self, type)
-
end
-
1
alias_method :to_s, :to_xpath
-
end
-
end
-
1
module XPath
-
1
module HTML
-
1
include XPath::DSL::TopLevel
-
1
extend self
-
-
# Match an `a` link element.
-
#
-
# @param [String] locator
-
# Text, id, title, or image alt attribute of the link
-
#
-
1
def link(locator)
-
locator = locator.to_s
-
link = descendant(:a)[attr(:href)]
-
link[attr(:id).equals(locator) | string.n.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]]
-
end
-
-
# Match a `submit`, `image`, or `button` element.
-
#
-
# @param [String] locator
-
# Value, title, id, or image alt attribute of the button
-
#
-
1
def button(locator)
-
locator = locator.to_s
-
button = descendant(:input)[attr(:type).one_of('submit', 'reset', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator) | attr(:title).is(locator)]
-
button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | string.n.is(locator) | attr(:title).is(locator)]
-
button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)]
-
end
-
-
-
# Match anything returned by either {#link} or {#button}.
-
#
-
# @param [String] locator
-
# Text, id, title, or image alt attribute of the link or button
-
#
-
1
def link_or_button(locator)
-
link(locator) + button(locator)
-
end
-
-
-
# Match any `fieldset` element.
-
#
-
# @param [String] locator
-
# Legend or id of the fieldset
-
#
-
1
def fieldset(locator)
-
locator = locator.to_s
-
descendant(:fieldset)[attr(:id).equals(locator) | child(:legend)[string.n.is(locator)]]
-
end
-
-
-
# Match any `input`, `textarea`, or `select` element that doesn't have a
-
# type of `submit`, `image`, or `hidden`.
-
#
-
# @param [String] locator
-
# Label, id, or name of field to match
-
#
-
1
def field(locator)
-
locator = locator.to_s
-
xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')]
-
xpath = locate_field(xpath, locator)
-
xpath
-
end
-
-
-
# Match any `input` or `textarea` element that can be filled with text.
-
# This excludes any inputs with a type of `submit`, `image`, `radio`,
-
# `checkbox`, `hidden`, or `file`.
-
#
-
# @param [String] locator
-
# Label, id, or name of field to match
-
#
-
1
def fillable_field(locator)
-
locator = locator.to_s
-
xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
-
xpath = locate_field(xpath, locator)
-
xpath
-
end
-
-
-
# Match any `select` element.
-
#
-
# @param [String] locator
-
# Label, id, or name of the field to match
-
#
-
1
def select(locator)
-
locator = locator.to_s
-
locate_field(descendant(:select), locator)
-
end
-
-
-
# Match any `input` element of type `checkbox`.
-
#
-
# @param [String] locator
-
# Label, id, or name of the checkbox to match
-
#
-
1
def checkbox(locator)
-
locator = locator.to_s
-
locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
-
end
-
-
-
# Match any `input` element of type `radio`.
-
#
-
# @param [String] locator
-
# Label, id, or name of the radio button to match
-
#
-
1
def radio_button(locator)
-
locator = locator.to_s
-
locate_field(descendant(:input)[attr(:type).equals('radio')], locator)
-
end
-
-
-
# Match any `input` element of type `file`.
-
#
-
# @param [String] locator
-
# Label, id, or name of the file field to match
-
#
-
1
def file_field(locator)
-
locator = locator.to_s
-
locate_field(descendant(:input)[attr(:type).equals('file')], locator)
-
end
-
-
-
# Match an `optgroup` element.
-
#
-
# @param [String] name
-
# Label for the option group
-
#
-
1
def optgroup(locator)
-
locator = locator.to_s
-
descendant(:optgroup)[attr(:label).is(locator)]
-
end
-
-
-
# Match an `option` element.
-
#
-
# @param [String] name
-
# Visible text of the option
-
#
-
1
def option(locator)
-
locator = locator.to_s
-
descendant(:option)[string.n.is(locator)]
-
end
-
-
-
# Match any `table` element.
-
#
-
# @param [String] locator
-
# Caption or id of the table to match
-
# @option options [Array] :rows
-
# Content of each cell in each row to match
-
#
-
1
def table(locator)
-
locator = locator.to_s
-
descendant(:table)[attr(:id).equals(locator) | descendant(:caption).is(locator)]
-
end
-
-
# Match any 'dd' element.
-
#
-
# @param [String] locator
-
# Id of the 'dd' element or text from preciding 'dt' element content
-
1
def definition_description(locator)
-
locator = locator.to_s
-
descendant(:dd)[attr(:id).equals(locator) | previous_sibling(:dt)[string.n.equals(locator)] ]
-
end
-
-
1
protected
-
-
1
def locate_field(xpath, locator)
-
locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:placeholder).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.is(locator)].attr(:for))]
-
locate_field += descendant(:label)[string.n.is(locator)].descendant(xpath)
-
locate_field
-
end
-
end
-
end
-
1
module XPath
-
1
class Literal
-
1
attr_reader :value
-
1
def initialize(value)
-
@value = value
-
end
-
end
-
end
-
1
module XPath
-
1
class Renderer
-
1
def self.render(node, type)
-
77
new(type).render(node)
-
end
-
-
1
def initialize(type)
-
77
@type = type
-
end
-
-
1
def render(node)
-
10373
arguments = node.arguments.map { |argument| convert_argument(argument) }
-
4250
send(node.expression, *arguments)
-
end
-
-
1
def convert_argument(argument)
-
7426
case argument
-
4173
when Expression, Union then render(argument)
-
1826
when Array then argument.map { |element| convert_argument(element) }
-
1293
when String then string_literal(argument)
-
when Literal then argument.value
-
1437
else argument.to_s
-
end
-
end
-
-
1
def string_literal(string)
-
1293
if string.include?("'")
-
string = string.split("'", -1).map do |substr|
-
"'#{substr}'"
-
end.join(%q{,"'",})
-
"concat(#{string})"
-
else
-
1293
"'#{string}'"
-
end
-
end
-
-
1
def this_node
-
959
'.'
-
end
-
-
1
def descendant(parent, element_names)
-
324
if element_names.length == 1
-
202
"#{parent}//#{element_names.first}"
-
122
elsif element_names.length > 1
-
366
"#{parent}//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
-
else
-
"#{parent}//*"
-
end
-
end
-
-
1
def child(parent, element_names)
-
if element_names.length == 1
-
"#{parent}/#{element_names.first}"
-
elsif element_names.length > 1
-
"#{parent}/*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
-
else
-
"#{parent}/*"
-
end
-
end
-
-
1
def axis(parent, name, tag_name)
-
"#{parent}/#{name}::#{tag_name}"
-
end
-
-
1
def node_name(current)
-
"name(#{current})"
-
end
-
-
1
def where(on, condition)
-
"#{on}[#{condition}]"
-
end
-
-
1
def attribute(current, name)
-
619
"#{current}/@#{name}"
-
end
-
-
1
def equality(one, two)
-
550
"#{one} = #{two}"
-
end
-
-
1
def is(one, two)
-
250
if @type == :exact
-
242
equality(one, two)
-
else
-
8
contains(one, two)
-
end
-
end
-
-
1
def variable(name)
-
"%{#{name}}"
-
end
-
-
1
def text(current)
-
"#{current}/text()"
-
end
-
-
1
def normalized_space(current)
-
138
"normalize-space(#{current})"
-
end
-
-
1
def literal(node)
-
node
-
end
-
-
1
def css(current, selector)
-
paths = Nokogiri::CSS.xpath_for(selector).map do |xpath_selector|
-
"#{current}#{xpath_selector}"
-
end
-
union(paths)
-
end
-
-
1
def union(*expressions)
-
109
expressions.join(' | ')
-
end
-
-
1
def anywhere(element_names)
-
61
if element_names.length == 1
-
61
"//#{element_names.first}"
-
elsif element_names.length > 1
-
"//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
-
else
-
"//*"
-
end
-
end
-
-
1
def contains(current, value)
-
8
"contains(#{current}, #{value})"
-
end
-
-
1
def starts_with(current, value)
-
"starts-with(#{current}, #{value})"
-
end
-
-
1
def and(one, two)
-
"(#{one} and #{two})"
-
end
-
-
1
def or(one, two)
-
279
"(#{one} or #{two})"
-
end
-
-
1
def one_of(current, values)
-
934
values.map { |value| "#{current} = #{value}" }.join(' or ')
-
end
-
-
1
def next_sibling(current, element_names)
-
if element_names.length == 1
-
"#{current}/following-sibling::*[1]/self::#{element_names.first}"
-
elsif element_names.length > 1
-
"#{current}/following-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
-
else
-
"#{current}/following-sibling::*[1]/self::*"
-
end
-
end
-
-
1
def previous_sibling(current, element_names)
-
if element_names.length == 1
-
"#{current}/preceding-sibling::*[1]/self::#{element_names.first}"
-
elsif element_names.length > 1
-
"#{current}/preceding-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
-
else
-
"#{current}/preceding-sibling::*[1]/self::*"
-
end
-
end
-
-
1
def inverse(current)
-
122
"not(#{current})"
-
end
-
-
1
def string_function(current)
-
138
"string(#{current})"
-
end
-
end
-
end
-
1
module XPath
-
1
class Union
-
1
include Enumerable
-
-
1
attr_reader :expressions
-
1
alias_method :arguments, :expressions
-
-
1
def initialize(*expressions)
-
343
@expressions = expressions
-
end
-
-
1
def expression
-
109
:union
-
end
-
-
1
def each(&block)
-
arguments.each(&block)
-
end
-
-
1
def method_missing(*args)
-
702
XPath::Union.new(*arguments.map { |e| e.send(*args) })
-
end
-
-
1
def to_xpath(type=nil)
-
77
Renderer.render(self, type)
-
end
-
1
alias_method :to_s, :to_xpath
-
end
-
end
-
1
ENV['RACK_ENV'] ||= 'development'
-
-
1
require 'sinatra/base'
-
1
require 'sinatra/flash'
-
1
require 'sinatra/partial'
-
1
require 'slim'
-
1
require 'slim/include'
-
-
1
require_relative 'server'
-
1
require_relative 'controllers/users'
-
1
require_relative 'controllers/sessions'
-
-
1
require_relative 'data_mapper_setup'
-
-
-
1
require_relative 'helpers'
-
-
1
require_relative 'controllers/rooms'
-
-
-
1
include Helpers
-
1
class Makersbnb < Sinatra::Base
-
-
1
get '/rooms/new' do
-
1
slim :'rooms/new'
-
end
-
-
1
post '/rooms' do
-
-
1
room = Room.create(name: params[:name],
-
address: params[:address],
-
description: params[:description],
-
price: params[:price],
-
user: current_user)
-
1
"Room Listed" # TODO update this to a path later when we
-
# decide where it will redirect following room listing
-
-
end
-
-
end
-
1
class Makersbnb < Sinatra::Base
-
-
1
get '/sessions/new' do
-
-
1
slim :'/sessions/new'
-
-
end
-
-
1
post '/sessions' do
-
1
user = User.authenticate(params[:email], params[:password])
-
1
if user
-
1
session[:user_id] = user.id
-
1
redirect '/'
-
else
-
flash.keep[:notice] = "The email or password entered is incorrect"
-
redirect '/sessions/new'
-
end
-
end
-
-
1
delete '/sessions' do
-
2
session[:user_id] = nil
-
2
flash.keep[:notice] = 'Goodbye!'
-
2
redirect '/'
-
end
-
-
-
end
-
1
class Makersbnb < Sinatra::Base
-
-
1
get '/users/new' do
-
22
@user = User.new
-
22
slim :'users/new'
-
end
-
-
1
post '/users' do
-
11
@user = User.create(email: params[:email],
-
name: params[:name],
-
user_name: params[:username],
-
password: params[:password],
-
password_confirmation: params[:password_confirmation])
-
11
@user.save
-
11
session[:user_id] = @user.id
-
11
redirect '/users/new'
-
end
-
end
-
1
require 'data_mapper'
-
1
require 'dm-postgres-adapter'
-
-
1
require_relative 'models/booking'
-
1
require_relative 'models/date'
-
1
require_relative 'models/room'
-
1
require_relative 'models/user'
-
-
1
DataMapper.setup(:default, ENV['DATABASE_URL'] || "postgres://localhost/makers_bnb_#{ENV['RACK_ENV']}")
-
1
DataMapper.finalize
-
1
DataMapper.auto_upgrade!
-
1
module Helpers
-
-
1
def current_user
-
51
@current_user ||= User.get(session[:user_id]) # TODO - incorporate this into sessions controller
-
end
-
-
1
def error?
-
flash[:errors] && !flash[:errors].empty? # TODO - incorporate this into controller
-
end
-
-
end
-
1
class Booking
-
1
include DataMapper::Resource
-
-
1
property :id, Serial
-
1
property :confirmed, Boolean
-
-
1
belongs_to :user, required: true
-
1
belongs_to :room, required: true
-
1
belongs_to :calendardate, required: true
-
-
end
-
1
class Calendardate
-
1
include DataMapper::Resource
-
-
1
property :id, Serial
-
1
property :date, Date, required: true
-
-
1
has n, :rooms, through: Resource
-
-
end
-
1
class Room
-
1
include DataMapper::Resource
-
-
1
property :id, Serial
-
1
property :name, String, required: true
-
1
property :address, Text, required: true, unique: true
-
1
property :description, Text
-
1
property :price, Integer, required: true
-
-
1
belongs_to :user
-
1
has n, :calendardates, through: Resource
-
-
1
def dates_booked
-
1
dates = []
-
3
calendardates.each { |d| dates << d }
-
end
-
-
1
def owner
-
1
user
-
end
-
-
1
def booked?(date)
-
5
calendardates.any? { |d| d == date}
-
end
-
end
-
1
require 'bcrypt'
-
-
1
class User
-
1
include DataMapper::Resource
-
-
1
attr_reader :password
-
1
attr_accessor :password_confirmation
-
-
1
property :id, Serial
-
1
property :email, String,required: true, format: :email_address, unique: true
-
1
property :name, String,required: true
-
1
property :user_name, String,required: true, unique: true
-
1
property :password_digest,String,required: true, length: 60
-
-
1
validates_confirmation_of :password
-
-
1
has n, :rooms
-
1
has n, :bookings
-
-
1
def password=(password)
-
11
@password = password
-
11
self.password_digest = BCrypt::Password.create(password)
-
end
-
-
1
def self.authenticate(email, password)
-
1
user = first(email: email)
-
1
if user && BCrypt::Password.new(user.password_digest) == password
-
1
user
-
else
-
nil
-
end
-
end
-
-
end
-
1
class Makersbnb < Sinatra::Base
-
-
1
register Sinatra::Flash
-
1
register Sinatra::Partial
-
-
1
use Rack::MethodOverride
-
-
1
enable :sessions
-
1
set :session_secret, 'super secret'
-
-
1
set :partial_template_engine, :slim
-
1
enable :partial_underscores
-
-
1
Slim::Engine.set_options shortcut: {'&' => {tag: 'input', attr: 'type'}, '#' => {attr: 'id'}, '.' => {attr: 'class'}, '@' => {attr: 'role'}, '>' => {tag: 'form', attr: 'method'}, '<' => {tag: 'form', attr: 'method'} }
-
-
1
get '/' do
-
4
slim :index
-
end
-
-
# start the server if ruby file executed directly
-
1
run! if app_file == $0
-
end
-
1
feature 'Listing a room' do
-
1
scenario 'As a landlord, I can list a new room when signed in' do
-
1
sign_up #TODO: uncomment this when signup works
-
# expect(page).to have_button 'New Room' TODO: uncomment this when signup works and welcome.slim has this button
-
2
expect{list_room}.to change(Room, :count).by(1)
-
#expect(page).to have_content 'Room Listed'
-
end
-
1
scenario 'I cannot list a new room when not signed in' do
-
1
expect(page).not_to have_button 'New Room'
-
end
-
end
-
1
feature 'Sign in' do
-
1
xscenario 'I cannot sign in if I have not signed up' do
-
sign_in
-
expect(current_path).to eq '/sessions/new'
-
expect(page).to have_content('The email or password entered is incorrect')
-
end
-
1
scenario 'I can sign in (once I have signed up)' do
-
1
sign_up
-
1
sign_out
-
1
sign_in
-
1
expect(current_path).to eq '/'
-
1
expect(page).to have_content('Welcome, test')
-
end
-
1
xscenario 'I cannot sign in if I enter my email incorrectly' do
-
sign_up
-
sign_in(email: 'something@something.com')
-
expect(current_path).to eq '/sessions/new'
-
expect(page).to have_content('The email or password entered is incorrect')
-
end
-
1
xscenario 'I cannot sign in if I enter my password incorrectly' do
-
sign_up
-
sign_in(password: 'something')
-
expect(current_path).to eq '/sessions/new'
-
expect(page).to have_content('The email or password entered is incorrect')
-
end
-
end
-
1
feature 'Sign out' do
-
1
scenario 'I can sign out once I have signed up' do
-
1
visit '/'
-
1
sign_up
-
1
sign_out
-
#expect(page).to have_content('Goodbye!')
-
end
-
end
-
# require_relative 'web_helper'
-
-
1
feature "Registration of a new user" do
-
-
1
scenario "Sign-up increases user count by one" do
-
2
expect { sign_up }.to change(User, :count).by(1)
-
end
-
-
1
scenario 'I can sign up as a new user' do
-
1
sign_up
-
1
expect(page).to have_content('Welcome, test')
-
1
expect(User.first.email).to eq('test@test.com')
-
end
-
-
1
scenario 'Requires a matching confirmation password' do
-
2
expect { sign_up(password_confirmation: 'wrong') }.not_to change(User, :count)
-
# expect(current_path).to eq('/users')
-
# expect(page).to have_content('Password and confirmation password do not match')
-
end
-
-
1
scenario 'User cannot sign up without an email address' do
-
2
expect { sign_up(email: nil) }.not_to change(User, :count)
-
end
-
-
1
scenario "User can't sign up with the same email address" do
-
1
sign_up
-
2
expect { sign_up(username: 'test02') }.not_to change(User, :count)
-
end
-
-
1
scenario "User can't sign up with the same user name" do
-
1
sign_up
-
2
expect { sign_up(email: 'test2@test.com') }.not_to change(User, :count)
-
end
-
end
-
1
def sign_up(name: 'test',
-
email: 'test@test.com',
-
username: 'test01',
-
password: '12345678',
-
password_confirmation: '12345678')
-
11
visit '/users/new'
-
-
11
fill_in :name, with: name
-
11
fill_in :email, with: email
-
11
fill_in :username, with: username
-
11
fill_in :password, with: password
-
11
fill_in :password_confirmation, with: password_confirmation
-
11
click_button 'Sign Up'
-
end
-
-
1
def sign_out()
-
2
click_button 'Sign out'
-
end
-
1
def list_room
-
1
visit '/rooms/new'
-
1
fill_in :name, with: 'The Penthouse'
-
1
fill_in :address, with: '1 Fake Road, Fakesville, Notrealshire, FA43 123'
-
1
fill_in :description, with: 'Lovely non-existant location, very quiet'
-
1
fill_in :price, with: '500'
-
1
click_button 'List Room'
-
end
-
1
def sign_in(email: 'test@test.com',
-
password: '12345678')
-
1
visit '/sessions/new'
-
1
fill_in :email, with: email
-
1
fill_in :password, with: password
-
1
click_button 'Sign in'
-
end
-
1
describe Room do
-
-
1
let!(:room) do
-
4
Room.create(name: "new_room",
-
address: "1 Fake Road, Fakesville, Notrealshire, FA43 123",
-
description: "Lovely non-existant location, very quiet",
-
price: 500)
-
end
-
-
1
let!(:date1) do
-
4
create_date("2017-02-01")
-
end
-
-
1
let!(:date2) do
-
4
create_date("2017-02-02")
-
end
-
-
1
let!(:date3) do
-
4
create_date("2017-02-03")
-
end
-
-
1
let!(:user) do
-
4
User.create(name: "Michael Jackson",
-
email: "michael@jackson.com",
-
user_name: "mj")
-
end
-
-
1
before do
-
4
dates = [date1, date2]
-
12
dates.each { |d| room.calendardates << d }
-
4
room.save
-
-
4
user.rooms << room
-
4
user.save
-
end
-
-
1
it 'can return dates that it is booked' do
-
1
expect(room.dates_booked).to eq([date1, date2])
-
end
-
-
1
it "knows it's owner" do
-
1
expect(room.owner).to eq user
-
end
-
-
1
describe "#booked?" do
-
-
1
it "returns true if it's booked on a certain date" do
-
1
expect(room.booked?(date1)).to eq true
-
end
-
-
1
it "returns false if it's not booked" do
-
1
expect(room.booked?(date3)).to eq false
-
end
-
end
-
end
-
-
1
def create_date(date)
-
12
Calendardate.create(date: Date.parse(date))
-
end